Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .gitattributes +5 -0
- .venv/lib/python3.11/site-packages/google/auth/__init__.py +53 -0
- .venv/lib/python3.11/site-packages/google/auth/_cloud_sdk.py +153 -0
- .venv/lib/python3.11/site-packages/google/auth/_credentials_async.py +171 -0
- .venv/lib/python3.11/site-packages/google/auth/_exponential_backoff.py +164 -0
- .venv/lib/python3.11/site-packages/google/auth/api_key.py +76 -0
- .venv/lib/python3.11/site-packages/google/auth/aws.py +861 -0
- .venv/lib/python3.11/site-packages/google/auth/compute_engine/__pycache__/credentials.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/google/auth/downscoped.py +512 -0
- .venv/lib/python3.11/site-packages/google/auth/external_account_authorized_user.py +380 -0
- .venv/lib/python3.11/site-packages/google/auth/transport/_custom_tls_signer.py +283 -0
- .venv/lib/python3.11/site-packages/google/auth/transport/_requests_base.py +53 -0
- .venv/lib/python3.11/site-packages/google/auth/transport/grpc.py +343 -0
- .venv/lib/python3.11/site-packages/google/oauth2/__init__.py +36 -0
- .venv/lib/python3.11/site-packages/google/oauth2/__pycache__/__init__.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/google/oauth2/__pycache__/_client.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/google/oauth2/__pycache__/_client_async.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/google/oauth2/__pycache__/_credentials_async.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/google/oauth2/__pycache__/_id_token_async.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/google/oauth2/__pycache__/_reauth_async.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/google/oauth2/__pycache__/_service_account_async.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/google/oauth2/__pycache__/challenges.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/google/oauth2/__pycache__/credentials.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/google/oauth2/__pycache__/gdch_credentials.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/google/oauth2/__pycache__/id_token.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/google/oauth2/__pycache__/reauth.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/google/oauth2/__pycache__/service_account.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/google/oauth2/__pycache__/sts.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/google/oauth2/__pycache__/utils.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/google/oauth2/__pycache__/webauthn_handler.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/google/oauth2/__pycache__/webauthn_handler_factory.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/google/oauth2/__pycache__/webauthn_types.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/google/oauth2/_client.py +508 -0
- .venv/lib/python3.11/site-packages/google/oauth2/_client_async.py +286 -0
- .venv/lib/python3.11/site-packages/google/oauth2/_credentials_async.py +118 -0
- .venv/lib/python3.11/site-packages/google/oauth2/_id_token_async.py +285 -0
- .venv/lib/python3.11/site-packages/google/oauth2/_reauth_async.py +328 -0
- .venv/lib/python3.11/site-packages/google/oauth2/_service_account_async.py +132 -0
- .venv/lib/python3.11/site-packages/google/oauth2/challenges.py +281 -0
- .venv/lib/python3.11/site-packages/google/oauth2/credentials.py +614 -0
- .venv/lib/python3.11/site-packages/google/oauth2/gdch_credentials.py +251 -0
- .venv/lib/python3.11/site-packages/google/oauth2/id_token.py +358 -0
- .venv/lib/python3.11/site-packages/google/oauth2/py.typed +2 -0
- .venv/lib/python3.11/site-packages/google/oauth2/reauth.py +369 -0
- .venv/lib/python3.11/site-packages/google/oauth2/service_account.py +847 -0
- .venv/lib/python3.11/site-packages/google/oauth2/sts.py +176 -0
- .venv/lib/python3.11/site-packages/google/oauth2/utils.py +168 -0
- .venv/lib/python3.11/site-packages/google/oauth2/webauthn_handler.py +82 -0
- .venv/lib/python3.11/site-packages/google/oauth2/webauthn_types.py +156 -0
- .venv/lib/python3.11/site-packages/google/type/calendar_period_pb2.py +44 -0
.gitattributes
CHANGED
|
@@ -186,3 +186,8 @@ tuning-competition-baseline/.venv/lib/python3.11/site-packages/torch/_inductor/_
|
|
| 186 |
.venv/lib/python3.11/site-packages/ray/scripts/__pycache__/scripts.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
|
| 187 |
.venv/lib/python3.11/site-packages/cpuinfo/__pycache__/cpuinfo.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
|
| 188 |
.venv/lib/python3.11/site-packages/yaml/_yaml.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
.venv/lib/python3.11/site-packages/ray/scripts/__pycache__/scripts.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
|
| 187 |
.venv/lib/python3.11/site-packages/cpuinfo/__pycache__/cpuinfo.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
|
| 188 |
.venv/lib/python3.11/site-packages/yaml/_yaml.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
|
| 189 |
+
.venv/lib/python3.11/site-packages/pillow.libs/libbrotlicommon-3ecfe81c.so.1 filter=lfs diff=lfs merge=lfs -text
|
| 190 |
+
.venv/lib/python3.11/site-packages/pillow.libs/libwebp-2fd3cdca.so.7.1.9 filter=lfs diff=lfs merge=lfs -text
|
| 191 |
+
.venv/lib/python3.11/site-packages/pillow.libs/libfreetype-be14bf51.so.6.20.1 filter=lfs diff=lfs merge=lfs -text
|
| 192 |
+
.venv/lib/python3.11/site-packages/pillow.libs/libpng16-58efbb84.so.16.43.0 filter=lfs diff=lfs merge=lfs -text
|
| 193 |
+
.venv/lib/python3.11/site-packages/pillow.libs/libjpeg-77ae51ab.so.62.4.0 filter=lfs diff=lfs merge=lfs -text
|
.venv/lib/python3.11/site-packages/google/auth/__init__.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2016 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""Google Auth Library for Python."""
|
| 16 |
+
|
| 17 |
+
import logging
|
| 18 |
+
import sys
|
| 19 |
+
import warnings
|
| 20 |
+
|
| 21 |
+
from google.auth import version as google_auth_version
|
| 22 |
+
from google.auth._default import (
|
| 23 |
+
default,
|
| 24 |
+
load_credentials_from_dict,
|
| 25 |
+
load_credentials_from_file,
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
__version__ = google_auth_version.__version__
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
__all__ = ["default", "load_credentials_from_file", "load_credentials_from_dict"]
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class Python37DeprecationWarning(DeprecationWarning): # pragma: NO COVER
|
| 36 |
+
"""
|
| 37 |
+
Deprecation warning raised when Python 3.7 runtime is detected.
|
| 38 |
+
Python 3.7 support will be dropped after January 1, 2024.
|
| 39 |
+
"""
|
| 40 |
+
|
| 41 |
+
pass
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
# Checks if the current runtime is Python 3.7.
|
| 45 |
+
if sys.version_info.major == 3 and sys.version_info.minor == 7: # pragma: NO COVER
|
| 46 |
+
message = (
|
| 47 |
+
"After January 1, 2024, new releases of this library will drop support "
|
| 48 |
+
"for Python 3.7."
|
| 49 |
+
)
|
| 50 |
+
warnings.warn(message, Python37DeprecationWarning)
|
| 51 |
+
|
| 52 |
+
# Set default logging handler to avoid "No handler found" warnings.
|
| 53 |
+
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
.venv/lib/python3.11/site-packages/google/auth/_cloud_sdk.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2015 Google Inc.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""Helpers for reading the Google Cloud SDK's configuration."""
|
| 16 |
+
|
| 17 |
+
import os
|
| 18 |
+
import subprocess
|
| 19 |
+
|
| 20 |
+
from google.auth import _helpers
|
| 21 |
+
from google.auth import environment_vars
|
| 22 |
+
from google.auth import exceptions
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
# The ~/.config subdirectory containing gcloud credentials.
|
| 26 |
+
_CONFIG_DIRECTORY = "gcloud"
|
| 27 |
+
# Windows systems store config at %APPDATA%\gcloud
|
| 28 |
+
_WINDOWS_CONFIG_ROOT_ENV_VAR = "APPDATA"
|
| 29 |
+
# The name of the file in the Cloud SDK config that contains default
|
| 30 |
+
# credentials.
|
| 31 |
+
_CREDENTIALS_FILENAME = "application_default_credentials.json"
|
| 32 |
+
# The name of the Cloud SDK shell script
|
| 33 |
+
_CLOUD_SDK_POSIX_COMMAND = "gcloud"
|
| 34 |
+
_CLOUD_SDK_WINDOWS_COMMAND = "gcloud.cmd"
|
| 35 |
+
# The command to get the Cloud SDK configuration
|
| 36 |
+
_CLOUD_SDK_CONFIG_GET_PROJECT_COMMAND = ("config", "get", "project")
|
| 37 |
+
# The command to get google user access token
|
| 38 |
+
_CLOUD_SDK_USER_ACCESS_TOKEN_COMMAND = ("auth", "print-access-token")
|
| 39 |
+
# Cloud SDK's application-default client ID
|
| 40 |
+
CLOUD_SDK_CLIENT_ID = (
|
| 41 |
+
"764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.apps.googleusercontent.com"
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def get_config_path():
|
| 46 |
+
"""Returns the absolute path the the Cloud SDK's configuration directory.
|
| 47 |
+
|
| 48 |
+
Returns:
|
| 49 |
+
str: The Cloud SDK config path.
|
| 50 |
+
"""
|
| 51 |
+
# If the path is explicitly set, return that.
|
| 52 |
+
try:
|
| 53 |
+
return os.environ[environment_vars.CLOUD_SDK_CONFIG_DIR]
|
| 54 |
+
except KeyError:
|
| 55 |
+
pass
|
| 56 |
+
|
| 57 |
+
# Non-windows systems store this at ~/.config/gcloud
|
| 58 |
+
if os.name != "nt":
|
| 59 |
+
return os.path.join(os.path.expanduser("~"), ".config", _CONFIG_DIRECTORY)
|
| 60 |
+
# Windows systems store config at %APPDATA%\gcloud
|
| 61 |
+
else:
|
| 62 |
+
try:
|
| 63 |
+
return os.path.join(
|
| 64 |
+
os.environ[_WINDOWS_CONFIG_ROOT_ENV_VAR], _CONFIG_DIRECTORY
|
| 65 |
+
)
|
| 66 |
+
except KeyError:
|
| 67 |
+
# This should never happen unless someone is really
|
| 68 |
+
# messing with things, but we'll cover the case anyway.
|
| 69 |
+
drive = os.environ.get("SystemDrive", "C:")
|
| 70 |
+
return os.path.join(drive, "\\", _CONFIG_DIRECTORY)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def get_application_default_credentials_path():
|
| 74 |
+
"""Gets the path to the application default credentials file.
|
| 75 |
+
|
| 76 |
+
The path may or may not exist.
|
| 77 |
+
|
| 78 |
+
Returns:
|
| 79 |
+
str: The full path to application default credentials.
|
| 80 |
+
"""
|
| 81 |
+
config_path = get_config_path()
|
| 82 |
+
return os.path.join(config_path, _CREDENTIALS_FILENAME)
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def _run_subprocess_ignore_stderr(command):
|
| 86 |
+
""" Return subprocess.check_output with the given command and ignores stderr."""
|
| 87 |
+
with open(os.devnull, "w") as devnull:
|
| 88 |
+
output = subprocess.check_output(command, stderr=devnull)
|
| 89 |
+
return output
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def get_project_id():
|
| 93 |
+
"""Gets the project ID from the Cloud SDK.
|
| 94 |
+
|
| 95 |
+
Returns:
|
| 96 |
+
Optional[str]: The project ID.
|
| 97 |
+
"""
|
| 98 |
+
if os.name == "nt":
|
| 99 |
+
command = _CLOUD_SDK_WINDOWS_COMMAND
|
| 100 |
+
else:
|
| 101 |
+
command = _CLOUD_SDK_POSIX_COMMAND
|
| 102 |
+
|
| 103 |
+
try:
|
| 104 |
+
# Ignore the stderr coming from gcloud, so it won't be mixed into the output.
|
| 105 |
+
# https://github.com/googleapis/google-auth-library-python/issues/673
|
| 106 |
+
project = _run_subprocess_ignore_stderr(
|
| 107 |
+
(command,) + _CLOUD_SDK_CONFIG_GET_PROJECT_COMMAND
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
# Turn bytes into a string and remove "\n"
|
| 111 |
+
project = _helpers.from_bytes(project).strip()
|
| 112 |
+
return project if project else None
|
| 113 |
+
except (subprocess.CalledProcessError, OSError, IOError):
|
| 114 |
+
return None
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def get_auth_access_token(account=None):
|
| 118 |
+
"""Load user access token with the ``gcloud auth print-access-token`` command.
|
| 119 |
+
|
| 120 |
+
Args:
|
| 121 |
+
account (Optional[str]): Account to get the access token for. If not
|
| 122 |
+
specified, the current active account will be used.
|
| 123 |
+
|
| 124 |
+
Returns:
|
| 125 |
+
str: The user access token.
|
| 126 |
+
|
| 127 |
+
Raises:
|
| 128 |
+
google.auth.exceptions.UserAccessTokenError: if failed to get access
|
| 129 |
+
token from gcloud.
|
| 130 |
+
"""
|
| 131 |
+
if os.name == "nt":
|
| 132 |
+
command = _CLOUD_SDK_WINDOWS_COMMAND
|
| 133 |
+
else:
|
| 134 |
+
command = _CLOUD_SDK_POSIX_COMMAND
|
| 135 |
+
|
| 136 |
+
try:
|
| 137 |
+
if account:
|
| 138 |
+
command = (
|
| 139 |
+
(command,)
|
| 140 |
+
+ _CLOUD_SDK_USER_ACCESS_TOKEN_COMMAND
|
| 141 |
+
+ ("--account=" + account,)
|
| 142 |
+
)
|
| 143 |
+
else:
|
| 144 |
+
command = (command,) + _CLOUD_SDK_USER_ACCESS_TOKEN_COMMAND
|
| 145 |
+
|
| 146 |
+
access_token = subprocess.check_output(command, stderr=subprocess.STDOUT)
|
| 147 |
+
# remove the trailing "\n"
|
| 148 |
+
return access_token.decode("utf-8").strip()
|
| 149 |
+
except (subprocess.CalledProcessError, OSError, IOError) as caught_exc:
|
| 150 |
+
new_exc = exceptions.UserAccessTokenError(
|
| 151 |
+
"Failed to obtain access token", caught_exc
|
| 152 |
+
)
|
| 153 |
+
raise new_exc from caught_exc
|
.venv/lib/python3.11/site-packages/google/auth/_credentials_async.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2020 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
"""Interfaces for credentials."""
|
| 17 |
+
|
| 18 |
+
import abc
|
| 19 |
+
import inspect
|
| 20 |
+
|
| 21 |
+
from google.auth import credentials
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class Credentials(credentials.Credentials, metaclass=abc.ABCMeta):
|
| 25 |
+
"""Async inherited credentials class from google.auth.credentials.
|
| 26 |
+
The added functionality is the before_request call which requires
|
| 27 |
+
async/await syntax.
|
| 28 |
+
All credentials have a :attr:`token` that is used for authentication and
|
| 29 |
+
may also optionally set an :attr:`expiry` to indicate when the token will
|
| 30 |
+
no longer be valid.
|
| 31 |
+
|
| 32 |
+
Most credentials will be :attr:`invalid` until :meth:`refresh` is called.
|
| 33 |
+
Credentials can do this automatically before the first HTTP request in
|
| 34 |
+
:meth:`before_request`.
|
| 35 |
+
|
| 36 |
+
Although the token and expiration will change as the credentials are
|
| 37 |
+
:meth:`refreshed <refresh>` and used, credentials should be considered
|
| 38 |
+
immutable. Various credentials will accept configuration such as private
|
| 39 |
+
keys, scopes, and other options. These options are not changeable after
|
| 40 |
+
construction. Some classes will provide mechanisms to copy the credentials
|
| 41 |
+
with modifications such as :meth:`ScopedCredentials.with_scopes`.
|
| 42 |
+
"""
|
| 43 |
+
|
| 44 |
+
async def before_request(self, request, method, url, headers):
|
| 45 |
+
"""Performs credential-specific before request logic.
|
| 46 |
+
|
| 47 |
+
Refreshes the credentials if necessary, then calls :meth:`apply` to
|
| 48 |
+
apply the token to the authentication header.
|
| 49 |
+
|
| 50 |
+
Args:
|
| 51 |
+
request (google.auth.transport.Request): The object used to make
|
| 52 |
+
HTTP requests.
|
| 53 |
+
method (str): The request's HTTP method or the RPC method being
|
| 54 |
+
invoked.
|
| 55 |
+
url (str): The request's URI or the RPC service's URI.
|
| 56 |
+
headers (Mapping): The request's headers.
|
| 57 |
+
"""
|
| 58 |
+
# pylint: disable=unused-argument
|
| 59 |
+
# (Subclasses may use these arguments to ascertain information about
|
| 60 |
+
# the http request.)
|
| 61 |
+
|
| 62 |
+
if not self.valid:
|
| 63 |
+
if inspect.iscoroutinefunction(self.refresh):
|
| 64 |
+
await self.refresh(request)
|
| 65 |
+
else:
|
| 66 |
+
self.refresh(request)
|
| 67 |
+
self.apply(headers)
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
class CredentialsWithQuotaProject(credentials.CredentialsWithQuotaProject):
|
| 71 |
+
"""Abstract base for credentials supporting ``with_quota_project`` factory"""
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
class AnonymousCredentials(credentials.AnonymousCredentials, Credentials):
|
| 75 |
+
"""Credentials that do not provide any authentication information.
|
| 76 |
+
|
| 77 |
+
These are useful in the case of services that support anonymous access or
|
| 78 |
+
local service emulators that do not use credentials. This class inherits
|
| 79 |
+
from the sync anonymous credentials file, but is kept if async credentials
|
| 80 |
+
is initialized and we would like anonymous credentials.
|
| 81 |
+
"""
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
class ReadOnlyScoped(credentials.ReadOnlyScoped, metaclass=abc.ABCMeta):
|
| 85 |
+
"""Interface for credentials whose scopes can be queried.
|
| 86 |
+
|
| 87 |
+
OAuth 2.0-based credentials allow limiting access using scopes as described
|
| 88 |
+
in `RFC6749 Section 3.3`_.
|
| 89 |
+
If a credential class implements this interface then the credentials either
|
| 90 |
+
use scopes in their implementation.
|
| 91 |
+
|
| 92 |
+
Some credentials require scopes in order to obtain a token. You can check
|
| 93 |
+
if scoping is necessary with :attr:`requires_scopes`::
|
| 94 |
+
|
| 95 |
+
if credentials.requires_scopes:
|
| 96 |
+
# Scoping is required.
|
| 97 |
+
credentials = _credentials_async.with_scopes(scopes=['one', 'two'])
|
| 98 |
+
|
| 99 |
+
Credentials that require scopes must either be constructed with scopes::
|
| 100 |
+
|
| 101 |
+
credentials = SomeScopedCredentials(scopes=['one', 'two'])
|
| 102 |
+
|
| 103 |
+
Or must copy an existing instance using :meth:`with_scopes`::
|
| 104 |
+
|
| 105 |
+
scoped_credentials = _credentials_async.with_scopes(scopes=['one', 'two'])
|
| 106 |
+
|
| 107 |
+
Some credentials have scopes but do not allow or require scopes to be set,
|
| 108 |
+
these credentials can be used as-is.
|
| 109 |
+
|
| 110 |
+
.. _RFC6749 Section 3.3: https://tools.ietf.org/html/rfc6749#section-3.3
|
| 111 |
+
"""
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
class Scoped(credentials.Scoped):
|
| 115 |
+
"""Interface for credentials whose scopes can be replaced while copying.
|
| 116 |
+
|
| 117 |
+
OAuth 2.0-based credentials allow limiting access using scopes as described
|
| 118 |
+
in `RFC6749 Section 3.3`_.
|
| 119 |
+
If a credential class implements this interface then the credentials either
|
| 120 |
+
use scopes in their implementation.
|
| 121 |
+
|
| 122 |
+
Some credentials require scopes in order to obtain a token. You can check
|
| 123 |
+
if scoping is necessary with :attr:`requires_scopes`::
|
| 124 |
+
|
| 125 |
+
if credentials.requires_scopes:
|
| 126 |
+
# Scoping is required.
|
| 127 |
+
credentials = _credentials_async.create_scoped(['one', 'two'])
|
| 128 |
+
|
| 129 |
+
Credentials that require scopes must either be constructed with scopes::
|
| 130 |
+
|
| 131 |
+
credentials = SomeScopedCredentials(scopes=['one', 'two'])
|
| 132 |
+
|
| 133 |
+
Or must copy an existing instance using :meth:`with_scopes`::
|
| 134 |
+
|
| 135 |
+
scoped_credentials = credentials.with_scopes(scopes=['one', 'two'])
|
| 136 |
+
|
| 137 |
+
Some credentials have scopes but do not allow or require scopes to be set,
|
| 138 |
+
these credentials can be used as-is.
|
| 139 |
+
|
| 140 |
+
.. _RFC6749 Section 3.3: https://tools.ietf.org/html/rfc6749#section-3.3
|
| 141 |
+
"""
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def with_scopes_if_required(credentials, scopes):
|
| 145 |
+
"""Creates a copy of the credentials with scopes if scoping is required.
|
| 146 |
+
|
| 147 |
+
This helper function is useful when you do not know (or care to know) the
|
| 148 |
+
specific type of credentials you are using (such as when you use
|
| 149 |
+
:func:`google.auth.default`). This function will call
|
| 150 |
+
:meth:`Scoped.with_scopes` if the credentials are scoped credentials and if
|
| 151 |
+
the credentials require scoping. Otherwise, it will return the credentials
|
| 152 |
+
as-is.
|
| 153 |
+
|
| 154 |
+
Args:
|
| 155 |
+
credentials (google.auth.credentials.Credentials): The credentials to
|
| 156 |
+
scope if necessary.
|
| 157 |
+
scopes (Sequence[str]): The list of scopes to use.
|
| 158 |
+
|
| 159 |
+
Returns:
|
| 160 |
+
google.auth._credentials_async.Credentials: Either a new set of scoped
|
| 161 |
+
credentials, or the passed in credentials instance if no scoping
|
| 162 |
+
was required.
|
| 163 |
+
"""
|
| 164 |
+
if isinstance(credentials, Scoped) and credentials.requires_scopes:
|
| 165 |
+
return credentials.with_scopes(scopes)
|
| 166 |
+
else:
|
| 167 |
+
return credentials
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
class Signing(credentials.Signing, metaclass=abc.ABCMeta):
|
| 171 |
+
"""Interface for credentials that can cryptographically sign messages."""
|
.venv/lib/python3.11/site-packages/google/auth/_exponential_backoff.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2022 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import asyncio
|
| 16 |
+
import random
|
| 17 |
+
import time
|
| 18 |
+
|
| 19 |
+
from google.auth import exceptions
|
| 20 |
+
|
| 21 |
+
# The default amount of retry attempts
|
| 22 |
+
_DEFAULT_RETRY_TOTAL_ATTEMPTS = 3
|
| 23 |
+
|
| 24 |
+
# The default initial backoff period (1.0 second).
|
| 25 |
+
_DEFAULT_INITIAL_INTERVAL_SECONDS = 1.0
|
| 26 |
+
|
| 27 |
+
# The default randomization factor (0.1 which results in a random period ranging
|
| 28 |
+
# between 10% below and 10% above the retry interval).
|
| 29 |
+
_DEFAULT_RANDOMIZATION_FACTOR = 0.1
|
| 30 |
+
|
| 31 |
+
# The default multiplier value (2 which is 100% increase per back off).
|
| 32 |
+
_DEFAULT_MULTIPLIER = 2.0
|
| 33 |
+
|
| 34 |
+
"""Exponential Backoff Utility
|
| 35 |
+
|
| 36 |
+
This is a private module that implements the exponential back off algorithm.
|
| 37 |
+
It can be used as a utility for code that needs to retry on failure, for example
|
| 38 |
+
an HTTP request.
|
| 39 |
+
"""
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class _BaseExponentialBackoff:
|
| 43 |
+
"""An exponential backoff iterator base class.
|
| 44 |
+
|
| 45 |
+
Args:
|
| 46 |
+
total_attempts Optional[int]:
|
| 47 |
+
The maximum amount of retries that should happen.
|
| 48 |
+
The default value is 3 attempts.
|
| 49 |
+
initial_wait_seconds Optional[int]:
|
| 50 |
+
The amount of time to sleep in the first backoff. This parameter
|
| 51 |
+
should be in seconds.
|
| 52 |
+
The default value is 1 second.
|
| 53 |
+
randomization_factor Optional[float]:
|
| 54 |
+
The amount of jitter that should be in each backoff. For example,
|
| 55 |
+
a value of 0.1 will introduce a jitter range of 10% to the
|
| 56 |
+
current backoff period.
|
| 57 |
+
The default value is 0.1.
|
| 58 |
+
multiplier Optional[float]:
|
| 59 |
+
The backoff multipler. This adjusts how much each backoff will
|
| 60 |
+
increase. For example a value of 2.0 leads to a 200% backoff
|
| 61 |
+
on each attempt. If the initial_wait is 1.0 it would look like
|
| 62 |
+
this sequence [1.0, 2.0, 4.0, 8.0].
|
| 63 |
+
The default value is 2.0.
|
| 64 |
+
"""
|
| 65 |
+
|
| 66 |
+
def __init__(
|
| 67 |
+
self,
|
| 68 |
+
total_attempts=_DEFAULT_RETRY_TOTAL_ATTEMPTS,
|
| 69 |
+
initial_wait_seconds=_DEFAULT_INITIAL_INTERVAL_SECONDS,
|
| 70 |
+
randomization_factor=_DEFAULT_RANDOMIZATION_FACTOR,
|
| 71 |
+
multiplier=_DEFAULT_MULTIPLIER,
|
| 72 |
+
):
|
| 73 |
+
if total_attempts < 1:
|
| 74 |
+
raise exceptions.InvalidValue(
|
| 75 |
+
f"total_attempts must be greater than or equal to 1 but was {total_attempts}"
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
self._total_attempts = total_attempts
|
| 79 |
+
self._initial_wait_seconds = initial_wait_seconds
|
| 80 |
+
|
| 81 |
+
self._current_wait_in_seconds = self._initial_wait_seconds
|
| 82 |
+
|
| 83 |
+
self._randomization_factor = randomization_factor
|
| 84 |
+
self._multiplier = multiplier
|
| 85 |
+
self._backoff_count = 0
|
| 86 |
+
|
| 87 |
+
@property
|
| 88 |
+
def total_attempts(self):
|
| 89 |
+
"""The total amount of backoff attempts that will be made."""
|
| 90 |
+
return self._total_attempts
|
| 91 |
+
|
| 92 |
+
@property
|
| 93 |
+
def backoff_count(self):
|
| 94 |
+
"""The current amount of backoff attempts that have been made."""
|
| 95 |
+
return self._backoff_count
|
| 96 |
+
|
| 97 |
+
def _reset(self):
|
| 98 |
+
self._backoff_count = 0
|
| 99 |
+
self._current_wait_in_seconds = self._initial_wait_seconds
|
| 100 |
+
|
| 101 |
+
def _calculate_jitter(self):
|
| 102 |
+
jitter_variance = self._current_wait_in_seconds * self._randomization_factor
|
| 103 |
+
jitter = random.uniform(
|
| 104 |
+
self._current_wait_in_seconds - jitter_variance,
|
| 105 |
+
self._current_wait_in_seconds + jitter_variance,
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
return jitter
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
class ExponentialBackoff(_BaseExponentialBackoff):
|
| 112 |
+
"""An exponential backoff iterator. This can be used in a for loop to
|
| 113 |
+
perform requests with exponential backoff.
|
| 114 |
+
"""
|
| 115 |
+
|
| 116 |
+
def __init__(self, *args, **kwargs):
|
| 117 |
+
super(ExponentialBackoff, self).__init__(*args, **kwargs)
|
| 118 |
+
|
| 119 |
+
def __iter__(self):
|
| 120 |
+
self._reset()
|
| 121 |
+
return self
|
| 122 |
+
|
| 123 |
+
def __next__(self):
|
| 124 |
+
if self._backoff_count >= self._total_attempts:
|
| 125 |
+
raise StopIteration
|
| 126 |
+
self._backoff_count += 1
|
| 127 |
+
|
| 128 |
+
if self._backoff_count <= 1:
|
| 129 |
+
return self._backoff_count
|
| 130 |
+
|
| 131 |
+
jitter = self._calculate_jitter()
|
| 132 |
+
|
| 133 |
+
time.sleep(jitter)
|
| 134 |
+
|
| 135 |
+
self._current_wait_in_seconds *= self._multiplier
|
| 136 |
+
return self._backoff_count
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
class AsyncExponentialBackoff(_BaseExponentialBackoff):
|
| 140 |
+
"""An async exponential backoff iterator. This can be used in a for loop to
|
| 141 |
+
perform async requests with exponential backoff.
|
| 142 |
+
"""
|
| 143 |
+
|
| 144 |
+
def __init__(self, *args, **kwargs):
|
| 145 |
+
super(AsyncExponentialBackoff, self).__init__(*args, **kwargs)
|
| 146 |
+
|
| 147 |
+
def __aiter__(self):
|
| 148 |
+
self._reset()
|
| 149 |
+
return self
|
| 150 |
+
|
| 151 |
+
async def __anext__(self):
|
| 152 |
+
if self._backoff_count >= self._total_attempts:
|
| 153 |
+
raise StopAsyncIteration
|
| 154 |
+
self._backoff_count += 1
|
| 155 |
+
|
| 156 |
+
if self._backoff_count <= 1:
|
| 157 |
+
return self._backoff_count
|
| 158 |
+
|
| 159 |
+
jitter = self._calculate_jitter()
|
| 160 |
+
|
| 161 |
+
await asyncio.sleep(jitter)
|
| 162 |
+
|
| 163 |
+
self._current_wait_in_seconds *= self._multiplier
|
| 164 |
+
return self._backoff_count
|
.venv/lib/python3.11/site-packages/google/auth/api_key.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2022 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""Google API key support.
|
| 16 |
+
This module provides authentication using the `API key`_.
|
| 17 |
+
.. _API key:
|
| 18 |
+
https://cloud.google.com/docs/authentication/api-keys/
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
from google.auth import _helpers
|
| 22 |
+
from google.auth import credentials
|
| 23 |
+
from google.auth import exceptions
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class Credentials(credentials.Credentials):
|
| 27 |
+
"""API key credentials.
|
| 28 |
+
These credentials use API key to provide authorization to applications.
|
| 29 |
+
"""
|
| 30 |
+
|
| 31 |
+
def __init__(self, token):
|
| 32 |
+
"""
|
| 33 |
+
Args:
|
| 34 |
+
token (str): API key string
|
| 35 |
+
Raises:
|
| 36 |
+
ValueError: If the provided API key is not a non-empty string.
|
| 37 |
+
"""
|
| 38 |
+
super(Credentials, self).__init__()
|
| 39 |
+
if not token:
|
| 40 |
+
raise exceptions.InvalidValue("Token must be a non-empty API key string")
|
| 41 |
+
self.token = token
|
| 42 |
+
|
| 43 |
+
@property
|
| 44 |
+
def expired(self):
|
| 45 |
+
return False
|
| 46 |
+
|
| 47 |
+
@property
|
| 48 |
+
def valid(self):
|
| 49 |
+
return True
|
| 50 |
+
|
| 51 |
+
@_helpers.copy_docstring(credentials.Credentials)
|
| 52 |
+
def refresh(self, request):
|
| 53 |
+
return
|
| 54 |
+
|
| 55 |
+
def apply(self, headers, token=None):
|
| 56 |
+
"""Apply the API key token to the x-goog-api-key header.
|
| 57 |
+
Args:
|
| 58 |
+
headers (Mapping): The HTTP request headers.
|
| 59 |
+
token (Optional[str]): If specified, overrides the current access
|
| 60 |
+
token.
|
| 61 |
+
"""
|
| 62 |
+
headers["x-goog-api-key"] = token or self.token
|
| 63 |
+
|
| 64 |
+
def before_request(self, request, method, url, headers):
|
| 65 |
+
"""Performs credential-specific before request logic.
|
| 66 |
+
Refreshes the credentials if necessary, then calls :meth:`apply` to
|
| 67 |
+
apply the token to the x-goog-api-key header.
|
| 68 |
+
Args:
|
| 69 |
+
request (google.auth.transport.Request): The object used to make
|
| 70 |
+
HTTP requests.
|
| 71 |
+
method (str): The request's HTTP method or the RPC method being
|
| 72 |
+
invoked.
|
| 73 |
+
url (str): The request's URI or the RPC service's URI.
|
| 74 |
+
headers (Mapping): The request's headers.
|
| 75 |
+
"""
|
| 76 |
+
self.apply(headers)
|
.venv/lib/python3.11/site-packages/google/auth/aws.py
ADDED
|
@@ -0,0 +1,861 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2020 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""AWS Credentials and AWS Signature V4 Request Signer.
|
| 16 |
+
|
| 17 |
+
This module provides credentials to access Google Cloud resources from Amazon
|
| 18 |
+
Web Services (AWS) workloads. These credentials are recommended over the
|
| 19 |
+
use of service account credentials in AWS as they do not involve the management
|
| 20 |
+
of long-live service account private keys.
|
| 21 |
+
|
| 22 |
+
AWS Credentials are initialized using external_account arguments which are
|
| 23 |
+
typically loaded from the external credentials JSON file.
|
| 24 |
+
|
| 25 |
+
This module also provides a definition for an abstract AWS security credentials supplier.
|
| 26 |
+
This supplier can be implemented to return valid AWS security credentials and an AWS region
|
| 27 |
+
and used to create AWS credentials. The credentials will then call the
|
| 28 |
+
supplier instead of using pre-defined methods such as calling the EC2 metadata endpoints.
|
| 29 |
+
|
| 30 |
+
This module also provides a basic implementation of the
|
| 31 |
+
`AWS Signature Version 4`_ request signing algorithm.
|
| 32 |
+
|
| 33 |
+
AWS Credentials use serialized signed requests to the
|
| 34 |
+
`AWS STS GetCallerIdentity`_ API that can be exchanged for Google access tokens
|
| 35 |
+
via the GCP STS endpoint.
|
| 36 |
+
|
| 37 |
+
.. _AWS Signature Version 4: https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
|
| 38 |
+
.. _AWS STS GetCallerIdentity: https://docs.aws.amazon.com/STS/latest/APIReference/API_GetCallerIdentity.html
|
| 39 |
+
"""
|
| 40 |
+
|
| 41 |
+
import abc
|
| 42 |
+
from dataclasses import dataclass
|
| 43 |
+
import hashlib
|
| 44 |
+
import hmac
|
| 45 |
+
import http.client as http_client
|
| 46 |
+
import json
|
| 47 |
+
import os
|
| 48 |
+
import posixpath
|
| 49 |
+
import re
|
| 50 |
+
from typing import Optional
|
| 51 |
+
import urllib
|
| 52 |
+
from urllib.parse import urljoin
|
| 53 |
+
|
| 54 |
+
from google.auth import _helpers
|
| 55 |
+
from google.auth import environment_vars
|
| 56 |
+
from google.auth import exceptions
|
| 57 |
+
from google.auth import external_account
|
| 58 |
+
|
| 59 |
+
# AWS Signature Version 4 signing algorithm identifier.
|
| 60 |
+
_AWS_ALGORITHM = "AWS4-HMAC-SHA256"
|
| 61 |
+
# The termination string for the AWS credential scope value as defined in
|
| 62 |
+
# https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
|
| 63 |
+
_AWS_REQUEST_TYPE = "aws4_request"
|
| 64 |
+
# The AWS authorization header name for the security session token if available.
|
| 65 |
+
_AWS_SECURITY_TOKEN_HEADER = "x-amz-security-token"
|
| 66 |
+
# The AWS authorization header name for the auto-generated date.
|
| 67 |
+
_AWS_DATE_HEADER = "x-amz-date"
|
| 68 |
+
# The default AWS regional credential verification URL.
|
| 69 |
+
_DEFAULT_AWS_REGIONAL_CREDENTIAL_VERIFICATION_URL = (
|
| 70 |
+
"https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15"
|
| 71 |
+
)
|
| 72 |
+
# IMDSV2 session token lifetime. This is set to a low value because the session token is used immediately.
|
| 73 |
+
_IMDSV2_SESSION_TOKEN_TTL_SECONDS = "300"
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
class RequestSigner(object):
|
| 77 |
+
"""Implements an AWS request signer based on the AWS Signature Version 4 signing
|
| 78 |
+
process.
|
| 79 |
+
https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
|
| 80 |
+
"""
|
| 81 |
+
|
| 82 |
+
def __init__(self, region_name):
|
| 83 |
+
"""Instantiates an AWS request signer used to compute authenticated signed
|
| 84 |
+
requests to AWS APIs based on the AWS Signature Version 4 signing process.
|
| 85 |
+
|
| 86 |
+
Args:
|
| 87 |
+
region_name (str): The AWS region to use.
|
| 88 |
+
"""
|
| 89 |
+
|
| 90 |
+
self._region_name = region_name
|
| 91 |
+
|
| 92 |
+
def get_request_options(
|
| 93 |
+
self,
|
| 94 |
+
aws_security_credentials,
|
| 95 |
+
url,
|
| 96 |
+
method,
|
| 97 |
+
request_payload="",
|
| 98 |
+
additional_headers={},
|
| 99 |
+
):
|
| 100 |
+
"""Generates the signed request for the provided HTTP request for calling
|
| 101 |
+
an AWS API. This follows the steps described at:
|
| 102 |
+
https://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html
|
| 103 |
+
|
| 104 |
+
Args:
|
| 105 |
+
aws_security_credentials (AWSSecurityCredentials): The AWS security credentials.
|
| 106 |
+
url (str): The AWS service URL containing the canonical URI and
|
| 107 |
+
query string.
|
| 108 |
+
method (str): The HTTP method used to call this API.
|
| 109 |
+
request_payload (Optional[str]): The optional request payload if
|
| 110 |
+
available.
|
| 111 |
+
additional_headers (Optional[Mapping[str, str]]): The optional
|
| 112 |
+
additional headers needed for the requested AWS API.
|
| 113 |
+
|
| 114 |
+
Returns:
|
| 115 |
+
Mapping[str, str]: The AWS signed request dictionary object.
|
| 116 |
+
"""
|
| 117 |
+
|
| 118 |
+
additional_headers = additional_headers or {}
|
| 119 |
+
|
| 120 |
+
uri = urllib.parse.urlparse(url)
|
| 121 |
+
# Normalize the URL path. This is needed for the canonical_uri.
|
| 122 |
+
# os.path.normpath can't be used since it normalizes "/" paths
|
| 123 |
+
# to "\\" in Windows OS.
|
| 124 |
+
normalized_uri = urllib.parse.urlparse(
|
| 125 |
+
urljoin(url, posixpath.normpath(uri.path))
|
| 126 |
+
)
|
| 127 |
+
# Validate provided URL.
|
| 128 |
+
if not uri.hostname or uri.scheme != "https":
|
| 129 |
+
raise exceptions.InvalidResource("Invalid AWS service URL")
|
| 130 |
+
|
| 131 |
+
header_map = _generate_authentication_header_map(
|
| 132 |
+
host=uri.hostname,
|
| 133 |
+
canonical_uri=normalized_uri.path or "/",
|
| 134 |
+
canonical_querystring=_get_canonical_querystring(uri.query),
|
| 135 |
+
method=method,
|
| 136 |
+
region=self._region_name,
|
| 137 |
+
aws_security_credentials=aws_security_credentials,
|
| 138 |
+
request_payload=request_payload,
|
| 139 |
+
additional_headers=additional_headers,
|
| 140 |
+
)
|
| 141 |
+
headers = {
|
| 142 |
+
"Authorization": header_map.get("authorization_header"),
|
| 143 |
+
"host": uri.hostname,
|
| 144 |
+
}
|
| 145 |
+
# Add x-amz-date if available.
|
| 146 |
+
if "amz_date" in header_map:
|
| 147 |
+
headers[_AWS_DATE_HEADER] = header_map.get("amz_date")
|
| 148 |
+
# Append additional optional headers, eg. X-Amz-Target, Content-Type, etc.
|
| 149 |
+
for key in additional_headers:
|
| 150 |
+
headers[key] = additional_headers[key]
|
| 151 |
+
|
| 152 |
+
# Add session token if available.
|
| 153 |
+
if aws_security_credentials.session_token is not None:
|
| 154 |
+
headers[_AWS_SECURITY_TOKEN_HEADER] = aws_security_credentials.session_token
|
| 155 |
+
|
| 156 |
+
signed_request = {"url": url, "method": method, "headers": headers}
|
| 157 |
+
if request_payload:
|
| 158 |
+
signed_request["data"] = request_payload
|
| 159 |
+
return signed_request
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def _get_canonical_querystring(query):
|
| 163 |
+
"""Generates the canonical query string given a raw query string.
|
| 164 |
+
Logic is based on
|
| 165 |
+
https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
|
| 166 |
+
|
| 167 |
+
Args:
|
| 168 |
+
query (str): The raw query string.
|
| 169 |
+
|
| 170 |
+
Returns:
|
| 171 |
+
str: The canonical query string.
|
| 172 |
+
"""
|
| 173 |
+
# Parse raw query string.
|
| 174 |
+
querystring = urllib.parse.parse_qs(query)
|
| 175 |
+
querystring_encoded_map = {}
|
| 176 |
+
for key in querystring:
|
| 177 |
+
quote_key = urllib.parse.quote(key, safe="-_.~")
|
| 178 |
+
# URI encode key.
|
| 179 |
+
querystring_encoded_map[quote_key] = []
|
| 180 |
+
for item in querystring[key]:
|
| 181 |
+
# For each key, URI encode all values for that key.
|
| 182 |
+
querystring_encoded_map[quote_key].append(
|
| 183 |
+
urllib.parse.quote(item, safe="-_.~")
|
| 184 |
+
)
|
| 185 |
+
# Sort values for each key.
|
| 186 |
+
querystring_encoded_map[quote_key].sort()
|
| 187 |
+
# Sort keys.
|
| 188 |
+
sorted_keys = list(querystring_encoded_map.keys())
|
| 189 |
+
sorted_keys.sort()
|
| 190 |
+
# Reconstruct the query string. Preserve keys with multiple values.
|
| 191 |
+
querystring_encoded_pairs = []
|
| 192 |
+
for key in sorted_keys:
|
| 193 |
+
for item in querystring_encoded_map[key]:
|
| 194 |
+
querystring_encoded_pairs.append("{}={}".format(key, item))
|
| 195 |
+
return "&".join(querystring_encoded_pairs)
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
def _sign(key, msg):
|
| 199 |
+
"""Creates the HMAC-SHA256 hash of the provided message using the provided
|
| 200 |
+
key.
|
| 201 |
+
|
| 202 |
+
Args:
|
| 203 |
+
key (str): The HMAC-SHA256 key to use.
|
| 204 |
+
msg (str): The message to hash.
|
| 205 |
+
|
| 206 |
+
Returns:
|
| 207 |
+
str: The computed hash bytes.
|
| 208 |
+
"""
|
| 209 |
+
return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest()
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
def _get_signing_key(key, date_stamp, region_name, service_name):
|
| 213 |
+
"""Calculates the signing key used to calculate the signature for
|
| 214 |
+
AWS Signature Version 4 based on:
|
| 215 |
+
https://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html
|
| 216 |
+
|
| 217 |
+
Args:
|
| 218 |
+
key (str): The AWS secret access key.
|
| 219 |
+
date_stamp (str): The '%Y%m%d' date format.
|
| 220 |
+
region_name (str): The AWS region.
|
| 221 |
+
service_name (str): The AWS service name, eg. sts.
|
| 222 |
+
|
| 223 |
+
Returns:
|
| 224 |
+
str: The signing key bytes.
|
| 225 |
+
"""
|
| 226 |
+
k_date = _sign(("AWS4" + key).encode("utf-8"), date_stamp)
|
| 227 |
+
k_region = _sign(k_date, region_name)
|
| 228 |
+
k_service = _sign(k_region, service_name)
|
| 229 |
+
k_signing = _sign(k_service, "aws4_request")
|
| 230 |
+
return k_signing
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
def _generate_authentication_header_map(
|
| 234 |
+
host,
|
| 235 |
+
canonical_uri,
|
| 236 |
+
canonical_querystring,
|
| 237 |
+
method,
|
| 238 |
+
region,
|
| 239 |
+
aws_security_credentials,
|
| 240 |
+
request_payload="",
|
| 241 |
+
additional_headers={},
|
| 242 |
+
):
|
| 243 |
+
"""Generates the authentication header map needed for generating the AWS
|
| 244 |
+
Signature Version 4 signed request.
|
| 245 |
+
|
| 246 |
+
Args:
|
| 247 |
+
host (str): The AWS service URL hostname.
|
| 248 |
+
canonical_uri (str): The AWS service URL path name.
|
| 249 |
+
canonical_querystring (str): The AWS service URL query string.
|
| 250 |
+
method (str): The HTTP method used to call this API.
|
| 251 |
+
region (str): The AWS region.
|
| 252 |
+
aws_security_credentials (AWSSecurityCredentials): The AWS security credentials.
|
| 253 |
+
request_payload (Optional[str]): The optional request payload if
|
| 254 |
+
available.
|
| 255 |
+
additional_headers (Optional[Mapping[str, str]]): The optional
|
| 256 |
+
additional headers needed for the requested AWS API.
|
| 257 |
+
|
| 258 |
+
Returns:
|
| 259 |
+
Mapping[str, str]: The AWS authentication header dictionary object.
|
| 260 |
+
This contains the x-amz-date and authorization header information.
|
| 261 |
+
"""
|
| 262 |
+
# iam.amazonaws.com host => iam service.
|
| 263 |
+
# sts.us-east-2.amazonaws.com host => sts service.
|
| 264 |
+
service_name = host.split(".")[0]
|
| 265 |
+
|
| 266 |
+
current_time = _helpers.utcnow()
|
| 267 |
+
amz_date = current_time.strftime("%Y%m%dT%H%M%SZ")
|
| 268 |
+
date_stamp = current_time.strftime("%Y%m%d")
|
| 269 |
+
|
| 270 |
+
# Change all additional headers to be lower case.
|
| 271 |
+
full_headers = {}
|
| 272 |
+
for key in additional_headers:
|
| 273 |
+
full_headers[key.lower()] = additional_headers[key]
|
| 274 |
+
# Add AWS session token if available.
|
| 275 |
+
if aws_security_credentials.session_token is not None:
|
| 276 |
+
full_headers[
|
| 277 |
+
_AWS_SECURITY_TOKEN_HEADER
|
| 278 |
+
] = aws_security_credentials.session_token
|
| 279 |
+
|
| 280 |
+
# Required headers
|
| 281 |
+
full_headers["host"] = host
|
| 282 |
+
# Do not use generated x-amz-date if the date header is provided.
|
| 283 |
+
# Previously the date was not fixed with x-amz- and could be provided
|
| 284 |
+
# manually.
|
| 285 |
+
# https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-header-value-trim.req
|
| 286 |
+
if "date" not in full_headers:
|
| 287 |
+
full_headers[_AWS_DATE_HEADER] = amz_date
|
| 288 |
+
|
| 289 |
+
# Header keys need to be sorted alphabetically.
|
| 290 |
+
canonical_headers = ""
|
| 291 |
+
header_keys = list(full_headers.keys())
|
| 292 |
+
header_keys.sort()
|
| 293 |
+
for key in header_keys:
|
| 294 |
+
canonical_headers = "{}{}:{}\n".format(
|
| 295 |
+
canonical_headers, key, full_headers[key]
|
| 296 |
+
)
|
| 297 |
+
signed_headers = ";".join(header_keys)
|
| 298 |
+
|
| 299 |
+
payload_hash = hashlib.sha256((request_payload or "").encode("utf-8")).hexdigest()
|
| 300 |
+
|
| 301 |
+
# https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
|
| 302 |
+
canonical_request = "{}\n{}\n{}\n{}\n{}\n{}".format(
|
| 303 |
+
method,
|
| 304 |
+
canonical_uri,
|
| 305 |
+
canonical_querystring,
|
| 306 |
+
canonical_headers,
|
| 307 |
+
signed_headers,
|
| 308 |
+
payload_hash,
|
| 309 |
+
)
|
| 310 |
+
|
| 311 |
+
credential_scope = "{}/{}/{}/{}".format(
|
| 312 |
+
date_stamp, region, service_name, _AWS_REQUEST_TYPE
|
| 313 |
+
)
|
| 314 |
+
|
| 315 |
+
# https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
|
| 316 |
+
string_to_sign = "{}\n{}\n{}\n{}".format(
|
| 317 |
+
_AWS_ALGORITHM,
|
| 318 |
+
amz_date,
|
| 319 |
+
credential_scope,
|
| 320 |
+
hashlib.sha256(canonical_request.encode("utf-8")).hexdigest(),
|
| 321 |
+
)
|
| 322 |
+
|
| 323 |
+
# https://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html
|
| 324 |
+
signing_key = _get_signing_key(
|
| 325 |
+
aws_security_credentials.secret_access_key, date_stamp, region, service_name
|
| 326 |
+
)
|
| 327 |
+
signature = hmac.new(
|
| 328 |
+
signing_key, string_to_sign.encode("utf-8"), hashlib.sha256
|
| 329 |
+
).hexdigest()
|
| 330 |
+
|
| 331 |
+
# https://docs.aws.amazon.com/general/latest/gr/sigv4-add-signature-to-request.html
|
| 332 |
+
authorization_header = "{} Credential={}/{}, SignedHeaders={}, Signature={}".format(
|
| 333 |
+
_AWS_ALGORITHM,
|
| 334 |
+
aws_security_credentials.access_key_id,
|
| 335 |
+
credential_scope,
|
| 336 |
+
signed_headers,
|
| 337 |
+
signature,
|
| 338 |
+
)
|
| 339 |
+
|
| 340 |
+
authentication_header = {"authorization_header": authorization_header}
|
| 341 |
+
# Do not use generated x-amz-date if the date header is provided.
|
| 342 |
+
if "date" not in full_headers:
|
| 343 |
+
authentication_header["amz_date"] = amz_date
|
| 344 |
+
return authentication_header
|
| 345 |
+
|
| 346 |
+
|
| 347 |
+
@dataclass
|
| 348 |
+
class AwsSecurityCredentials:
|
| 349 |
+
"""A class that models AWS security credentials with an optional session token.
|
| 350 |
+
|
| 351 |
+
Attributes:
|
| 352 |
+
access_key_id (str): The AWS security credentials access key id.
|
| 353 |
+
secret_access_key (str): The AWS security credentials secret access key.
|
| 354 |
+
session_token (Optional[str]): The optional AWS security credentials session token. This should be set when using temporary credentials.
|
| 355 |
+
"""
|
| 356 |
+
|
| 357 |
+
access_key_id: str
|
| 358 |
+
secret_access_key: str
|
| 359 |
+
session_token: Optional[str] = None
|
| 360 |
+
|
| 361 |
+
|
| 362 |
+
class AwsSecurityCredentialsSupplier(metaclass=abc.ABCMeta):
|
| 363 |
+
"""Base class for AWS security credential suppliers. This can be implemented with custom logic to retrieve
|
| 364 |
+
AWS security credentials to exchange for a Google Cloud access token. The AWS external account credential does
|
| 365 |
+
not cache the AWS security credentials, so caching logic should be added in the implementation.
|
| 366 |
+
"""
|
| 367 |
+
|
| 368 |
+
@abc.abstractmethod
|
| 369 |
+
def get_aws_security_credentials(self, context, request):
|
| 370 |
+
"""Returns the AWS security credentials for the requested context.
|
| 371 |
+
|
| 372 |
+
.. warning: This is not cached by the calling Google credential, so caching logic should be implemented in the supplier.
|
| 373 |
+
|
| 374 |
+
Args:
|
| 375 |
+
context (google.auth.externalaccount.SupplierContext): The context object
|
| 376 |
+
containing information about the requested audience and subject token type.
|
| 377 |
+
request (google.auth.transport.Request): The object used to make
|
| 378 |
+
HTTP requests.
|
| 379 |
+
|
| 380 |
+
Raises:
|
| 381 |
+
google.auth.exceptions.RefreshError: If an error is encountered during
|
| 382 |
+
security credential retrieval logic.
|
| 383 |
+
|
| 384 |
+
Returns:
|
| 385 |
+
AwsSecurityCredentials: The requested AWS security credentials.
|
| 386 |
+
"""
|
| 387 |
+
raise NotImplementedError("")
|
| 388 |
+
|
| 389 |
+
@abc.abstractmethod
|
| 390 |
+
def get_aws_region(self, context, request):
|
| 391 |
+
"""Returns the AWS region for the requested context.
|
| 392 |
+
|
| 393 |
+
Args:
|
| 394 |
+
context (google.auth.externalaccount.SupplierContext): The context object
|
| 395 |
+
containing information about the requested audience and subject token type.
|
| 396 |
+
request (google.auth.transport.Request): The object used to make
|
| 397 |
+
HTTP requests.
|
| 398 |
+
|
| 399 |
+
Raises:
|
| 400 |
+
google.auth.exceptions.RefreshError: If an error is encountered during
|
| 401 |
+
region retrieval logic.
|
| 402 |
+
|
| 403 |
+
Returns:
|
| 404 |
+
str: The AWS region.
|
| 405 |
+
"""
|
| 406 |
+
raise NotImplementedError("")
|
| 407 |
+
|
| 408 |
+
|
| 409 |
+
class _DefaultAwsSecurityCredentialsSupplier(AwsSecurityCredentialsSupplier):
|
| 410 |
+
"""Default implementation of AWS security credentials supplier. Supports retrieving
|
| 411 |
+
credentials and region via EC2 metadata endpoints and environment variables.
|
| 412 |
+
"""
|
| 413 |
+
|
| 414 |
+
def __init__(self, credential_source):
|
| 415 |
+
self._region_url = credential_source.get("region_url")
|
| 416 |
+
self._security_credentials_url = credential_source.get("url")
|
| 417 |
+
self._imdsv2_session_token_url = credential_source.get(
|
| 418 |
+
"imdsv2_session_token_url"
|
| 419 |
+
)
|
| 420 |
+
|
| 421 |
+
@_helpers.copy_docstring(AwsSecurityCredentialsSupplier)
|
| 422 |
+
def get_aws_security_credentials(self, context, request):
|
| 423 |
+
|
| 424 |
+
# Check environment variables for permanent credentials first.
|
| 425 |
+
# https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html
|
| 426 |
+
env_aws_access_key_id = os.environ.get(environment_vars.AWS_ACCESS_KEY_ID)
|
| 427 |
+
env_aws_secret_access_key = os.environ.get(
|
| 428 |
+
environment_vars.AWS_SECRET_ACCESS_KEY
|
| 429 |
+
)
|
| 430 |
+
# This is normally not available for permanent credentials.
|
| 431 |
+
env_aws_session_token = os.environ.get(environment_vars.AWS_SESSION_TOKEN)
|
| 432 |
+
if env_aws_access_key_id and env_aws_secret_access_key:
|
| 433 |
+
return AwsSecurityCredentials(
|
| 434 |
+
env_aws_access_key_id, env_aws_secret_access_key, env_aws_session_token
|
| 435 |
+
)
|
| 436 |
+
|
| 437 |
+
imdsv2_session_token = self._get_imdsv2_session_token(request)
|
| 438 |
+
role_name = self._get_metadata_role_name(request, imdsv2_session_token)
|
| 439 |
+
|
| 440 |
+
# Get security credentials.
|
| 441 |
+
credentials = self._get_metadata_security_credentials(
|
| 442 |
+
request, role_name, imdsv2_session_token
|
| 443 |
+
)
|
| 444 |
+
|
| 445 |
+
return AwsSecurityCredentials(
|
| 446 |
+
credentials.get("AccessKeyId"),
|
| 447 |
+
credentials.get("SecretAccessKey"),
|
| 448 |
+
credentials.get("Token"),
|
| 449 |
+
)
|
| 450 |
+
|
| 451 |
+
@_helpers.copy_docstring(AwsSecurityCredentialsSupplier)
|
| 452 |
+
def get_aws_region(self, context, request):
|
| 453 |
+
# The AWS metadata server is not available in some AWS environments
|
| 454 |
+
# such as AWS lambda. Instead, it is available via environment
|
| 455 |
+
# variable.
|
| 456 |
+
env_aws_region = os.environ.get(environment_vars.AWS_REGION)
|
| 457 |
+
if env_aws_region is not None:
|
| 458 |
+
return env_aws_region
|
| 459 |
+
|
| 460 |
+
env_aws_region = os.environ.get(environment_vars.AWS_DEFAULT_REGION)
|
| 461 |
+
if env_aws_region is not None:
|
| 462 |
+
return env_aws_region
|
| 463 |
+
|
| 464 |
+
if not self._region_url:
|
| 465 |
+
raise exceptions.RefreshError("Unable to determine AWS region")
|
| 466 |
+
|
| 467 |
+
headers = None
|
| 468 |
+
imdsv2_session_token = self._get_imdsv2_session_token(request)
|
| 469 |
+
if imdsv2_session_token is not None:
|
| 470 |
+
headers = {"X-aws-ec2-metadata-token": imdsv2_session_token}
|
| 471 |
+
|
| 472 |
+
response = request(url=self._region_url, method="GET", headers=headers)
|
| 473 |
+
|
| 474 |
+
# Support both string and bytes type response.data.
|
| 475 |
+
response_body = (
|
| 476 |
+
response.data.decode("utf-8")
|
| 477 |
+
if hasattr(response.data, "decode")
|
| 478 |
+
else response.data
|
| 479 |
+
)
|
| 480 |
+
|
| 481 |
+
if response.status != http_client.OK:
|
| 482 |
+
raise exceptions.RefreshError(
|
| 483 |
+
"Unable to retrieve AWS region: {}".format(response_body)
|
| 484 |
+
)
|
| 485 |
+
|
| 486 |
+
# This endpoint will return the region in format: us-east-2b.
|
| 487 |
+
# Only the us-east-2 part should be used.
|
| 488 |
+
return response_body[:-1]
|
| 489 |
+
|
| 490 |
+
def _get_imdsv2_session_token(self, request):
|
| 491 |
+
if request is not None and self._imdsv2_session_token_url is not None:
|
| 492 |
+
headers = {
|
| 493 |
+
"X-aws-ec2-metadata-token-ttl-seconds": _IMDSV2_SESSION_TOKEN_TTL_SECONDS
|
| 494 |
+
}
|
| 495 |
+
|
| 496 |
+
imdsv2_session_token_response = request(
|
| 497 |
+
url=self._imdsv2_session_token_url, method="PUT", headers=headers
|
| 498 |
+
)
|
| 499 |
+
|
| 500 |
+
if imdsv2_session_token_response.status != http_client.OK:
|
| 501 |
+
raise exceptions.RefreshError(
|
| 502 |
+
"Unable to retrieve AWS Session Token: {}".format(
|
| 503 |
+
imdsv2_session_token_response.data
|
| 504 |
+
)
|
| 505 |
+
)
|
| 506 |
+
|
| 507 |
+
return imdsv2_session_token_response.data
|
| 508 |
+
else:
|
| 509 |
+
return None
|
| 510 |
+
|
| 511 |
+
def _get_metadata_security_credentials(
|
| 512 |
+
self, request, role_name, imdsv2_session_token
|
| 513 |
+
):
|
| 514 |
+
"""Retrieves the AWS security credentials required for signing AWS
|
| 515 |
+
requests from the AWS metadata server.
|
| 516 |
+
|
| 517 |
+
Args:
|
| 518 |
+
request (google.auth.transport.Request): A callable used to make
|
| 519 |
+
HTTP requests.
|
| 520 |
+
role_name (str): The AWS role name required by the AWS metadata
|
| 521 |
+
server security_credentials endpoint in order to return the
|
| 522 |
+
credentials.
|
| 523 |
+
imdsv2_session_token (str): The AWS IMDSv2 session token to be added as a
|
| 524 |
+
header in the requests to AWS metadata endpoint.
|
| 525 |
+
|
| 526 |
+
Returns:
|
| 527 |
+
Mapping[str, str]: The AWS metadata server security credentials
|
| 528 |
+
response.
|
| 529 |
+
|
| 530 |
+
Raises:
|
| 531 |
+
google.auth.exceptions.RefreshError: If an error occurs while
|
| 532 |
+
retrieving the AWS security credentials.
|
| 533 |
+
"""
|
| 534 |
+
headers = {"Content-Type": "application/json"}
|
| 535 |
+
if imdsv2_session_token is not None:
|
| 536 |
+
headers["X-aws-ec2-metadata-token"] = imdsv2_session_token
|
| 537 |
+
|
| 538 |
+
response = request(
|
| 539 |
+
url="{}/{}".format(self._security_credentials_url, role_name),
|
| 540 |
+
method="GET",
|
| 541 |
+
headers=headers,
|
| 542 |
+
)
|
| 543 |
+
|
| 544 |
+
# support both string and bytes type response.data
|
| 545 |
+
response_body = (
|
| 546 |
+
response.data.decode("utf-8")
|
| 547 |
+
if hasattr(response.data, "decode")
|
| 548 |
+
else response.data
|
| 549 |
+
)
|
| 550 |
+
|
| 551 |
+
if response.status != http_client.OK:
|
| 552 |
+
raise exceptions.RefreshError(
|
| 553 |
+
"Unable to retrieve AWS security credentials: {}".format(response_body)
|
| 554 |
+
)
|
| 555 |
+
|
| 556 |
+
credentials_response = json.loads(response_body)
|
| 557 |
+
|
| 558 |
+
return credentials_response
|
| 559 |
+
|
| 560 |
+
def _get_metadata_role_name(self, request, imdsv2_session_token):
|
| 561 |
+
"""Retrieves the AWS role currently attached to the current AWS
|
| 562 |
+
workload by querying the AWS metadata server. This is needed for the
|
| 563 |
+
AWS metadata server security credentials endpoint in order to retrieve
|
| 564 |
+
the AWS security credentials needed to sign requests to AWS APIs.
|
| 565 |
+
|
| 566 |
+
Args:
|
| 567 |
+
request (google.auth.transport.Request): A callable used to make
|
| 568 |
+
HTTP requests.
|
| 569 |
+
imdsv2_session_token (str): The AWS IMDSv2 session token to be added as a
|
| 570 |
+
header in the requests to AWS metadata endpoint.
|
| 571 |
+
|
| 572 |
+
Returns:
|
| 573 |
+
str: The AWS role name.
|
| 574 |
+
|
| 575 |
+
Raises:
|
| 576 |
+
google.auth.exceptions.RefreshError: If an error occurs while
|
| 577 |
+
retrieving the AWS role name.
|
| 578 |
+
"""
|
| 579 |
+
if self._security_credentials_url is None:
|
| 580 |
+
raise exceptions.RefreshError(
|
| 581 |
+
"Unable to determine the AWS metadata server security credentials endpoint"
|
| 582 |
+
)
|
| 583 |
+
|
| 584 |
+
headers = None
|
| 585 |
+
if imdsv2_session_token is not None:
|
| 586 |
+
headers = {"X-aws-ec2-metadata-token": imdsv2_session_token}
|
| 587 |
+
|
| 588 |
+
response = request(
|
| 589 |
+
url=self._security_credentials_url, method="GET", headers=headers
|
| 590 |
+
)
|
| 591 |
+
|
| 592 |
+
# support both string and bytes type response.data
|
| 593 |
+
response_body = (
|
| 594 |
+
response.data.decode("utf-8")
|
| 595 |
+
if hasattr(response.data, "decode")
|
| 596 |
+
else response.data
|
| 597 |
+
)
|
| 598 |
+
|
| 599 |
+
if response.status != http_client.OK:
|
| 600 |
+
raise exceptions.RefreshError(
|
| 601 |
+
"Unable to retrieve AWS role name {}".format(response_body)
|
| 602 |
+
)
|
| 603 |
+
|
| 604 |
+
return response_body
|
| 605 |
+
|
| 606 |
+
|
| 607 |
+
class Credentials(external_account.Credentials):
|
| 608 |
+
"""AWS external account credentials.
|
| 609 |
+
This is used to exchange serialized AWS signature v4 signed requests to
|
| 610 |
+
AWS STS GetCallerIdentity service for Google access tokens.
|
| 611 |
+
"""
|
| 612 |
+
|
| 613 |
+
def __init__(
|
| 614 |
+
self,
|
| 615 |
+
audience,
|
| 616 |
+
subject_token_type,
|
| 617 |
+
token_url=external_account._DEFAULT_TOKEN_URL,
|
| 618 |
+
credential_source=None,
|
| 619 |
+
aws_security_credentials_supplier=None,
|
| 620 |
+
*args,
|
| 621 |
+
**kwargs
|
| 622 |
+
):
|
| 623 |
+
"""Instantiates an AWS workload external account credentials object.
|
| 624 |
+
|
| 625 |
+
Args:
|
| 626 |
+
audience (str): The STS audience field.
|
| 627 |
+
subject_token_type (str): The subject token type based on the Oauth2.0 token exchange spec.
|
| 628 |
+
Expected values include::
|
| 629 |
+
|
| 630 |
+
“urn:ietf:params:aws:token-type:aws4_request”
|
| 631 |
+
|
| 632 |
+
token_url (Optional [str]): The STS endpoint URL. If not provided, will default to "https://sts.googleapis.com/v1/token".
|
| 633 |
+
credential_source (Optional [Mapping]): The credential source dictionary used
|
| 634 |
+
to provide instructions on how to retrieve external credential to be exchanged for Google access tokens.
|
| 635 |
+
Either a credential source or an AWS security credentials supplier must be provided.
|
| 636 |
+
|
| 637 |
+
Example credential_source for AWS credential::
|
| 638 |
+
|
| 639 |
+
{
|
| 640 |
+
"environment_id": "aws1",
|
| 641 |
+
"regional_cred_verification_url": "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15",
|
| 642 |
+
"region_url": "http://169.254.169.254/latest/meta-data/placement/availability-zone",
|
| 643 |
+
"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials",
|
| 644 |
+
imdsv2_session_token_url": "http://169.254.169.254/latest/api/token"
|
| 645 |
+
}
|
| 646 |
+
|
| 647 |
+
aws_security_credentials_supplier (Optional [AwsSecurityCredentialsSupplier]): Optional AWS security credentials supplier.
|
| 648 |
+
This will be called to supply valid AWS security credentails which will then
|
| 649 |
+
be exchanged for Google access tokens. Either an AWS security credentials supplier
|
| 650 |
+
or a credential source must be provided.
|
| 651 |
+
args (List): Optional positional arguments passed into the underlying :meth:`~external_account.Credentials.__init__` method.
|
| 652 |
+
kwargs (Mapping): Optional keyword arguments passed into the underlying :meth:`~external_account.Credentials.__init__` method.
|
| 653 |
+
|
| 654 |
+
Raises:
|
| 655 |
+
google.auth.exceptions.RefreshError: If an error is encountered during
|
| 656 |
+
access token retrieval logic.
|
| 657 |
+
ValueError: For invalid parameters.
|
| 658 |
+
|
| 659 |
+
.. note:: Typically one of the helper constructors
|
| 660 |
+
:meth:`from_file` or
|
| 661 |
+
:meth:`from_info` are used instead of calling the constructor directly.
|
| 662 |
+
"""
|
| 663 |
+
super(Credentials, self).__init__(
|
| 664 |
+
audience=audience,
|
| 665 |
+
subject_token_type=subject_token_type,
|
| 666 |
+
token_url=token_url,
|
| 667 |
+
credential_source=credential_source,
|
| 668 |
+
*args,
|
| 669 |
+
**kwargs
|
| 670 |
+
)
|
| 671 |
+
if credential_source is None and aws_security_credentials_supplier is None:
|
| 672 |
+
raise exceptions.InvalidValue(
|
| 673 |
+
"A valid credential source or AWS security credentials supplier must be provided."
|
| 674 |
+
)
|
| 675 |
+
if (
|
| 676 |
+
credential_source is not None
|
| 677 |
+
and aws_security_credentials_supplier is not None
|
| 678 |
+
):
|
| 679 |
+
raise exceptions.InvalidValue(
|
| 680 |
+
"AWS credential cannot have both a credential source and an AWS security credentials supplier."
|
| 681 |
+
)
|
| 682 |
+
|
| 683 |
+
if aws_security_credentials_supplier:
|
| 684 |
+
self._aws_security_credentials_supplier = aws_security_credentials_supplier
|
| 685 |
+
# The regional cred verification URL would normally be provided through the credential source. So set it to the default one here.
|
| 686 |
+
self._cred_verification_url = (
|
| 687 |
+
_DEFAULT_AWS_REGIONAL_CREDENTIAL_VERIFICATION_URL
|
| 688 |
+
)
|
| 689 |
+
else:
|
| 690 |
+
environment_id = credential_source.get("environment_id") or ""
|
| 691 |
+
self._aws_security_credentials_supplier = _DefaultAwsSecurityCredentialsSupplier(
|
| 692 |
+
credential_source
|
| 693 |
+
)
|
| 694 |
+
self._cred_verification_url = credential_source.get(
|
| 695 |
+
"regional_cred_verification_url"
|
| 696 |
+
)
|
| 697 |
+
|
| 698 |
+
# Get the environment ID, i.e. "aws1". Currently, only one version supported (1).
|
| 699 |
+
matches = re.match(r"^(aws)([\d]+)$", environment_id)
|
| 700 |
+
if matches:
|
| 701 |
+
env_id, env_version = matches.groups()
|
| 702 |
+
else:
|
| 703 |
+
env_id, env_version = (None, None)
|
| 704 |
+
|
| 705 |
+
if env_id != "aws" or self._cred_verification_url is None:
|
| 706 |
+
raise exceptions.InvalidResource(
|
| 707 |
+
"No valid AWS 'credential_source' provided"
|
| 708 |
+
)
|
| 709 |
+
elif env_version is None or int(env_version) != 1:
|
| 710 |
+
raise exceptions.InvalidValue(
|
| 711 |
+
"aws version '{}' is not supported in the current build.".format(
|
| 712 |
+
env_version
|
| 713 |
+
)
|
| 714 |
+
)
|
| 715 |
+
|
| 716 |
+
self._target_resource = audience
|
| 717 |
+
self._request_signer = None
|
| 718 |
+
|
| 719 |
+
def retrieve_subject_token(self, request):
|
| 720 |
+
"""Retrieves the subject token using the credential_source object.
|
| 721 |
+
The subject token is a serialized `AWS GetCallerIdentity signed request`_.
|
| 722 |
+
|
| 723 |
+
The logic is summarized as:
|
| 724 |
+
|
| 725 |
+
Retrieve the AWS region from the AWS_REGION or AWS_DEFAULT_REGION
|
| 726 |
+
environment variable or from the AWS metadata server availability-zone
|
| 727 |
+
if not found in the environment variable.
|
| 728 |
+
|
| 729 |
+
Check AWS credentials in environment variables. If not found, retrieve
|
| 730 |
+
from the AWS metadata server security-credentials endpoint.
|
| 731 |
+
|
| 732 |
+
When retrieving AWS credentials from the metadata server
|
| 733 |
+
security-credentials endpoint, the AWS role needs to be determined by
|
| 734 |
+
calling the security-credentials endpoint without any argument. Then the
|
| 735 |
+
credentials can be retrieved via: security-credentials/role_name
|
| 736 |
+
|
| 737 |
+
Generate the signed request to AWS STS GetCallerIdentity action.
|
| 738 |
+
|
| 739 |
+
Inject x-goog-cloud-target-resource into header and serialize the
|
| 740 |
+
signed request. This will be the subject-token to pass to GCP STS.
|
| 741 |
+
|
| 742 |
+
.. _AWS GetCallerIdentity signed request:
|
| 743 |
+
https://cloud.google.com/iam/docs/access-resources-aws#exchange-token
|
| 744 |
+
|
| 745 |
+
Args:
|
| 746 |
+
request (google.auth.transport.Request): A callable used to make
|
| 747 |
+
HTTP requests.
|
| 748 |
+
Returns:
|
| 749 |
+
str: The retrieved subject token.
|
| 750 |
+
"""
|
| 751 |
+
|
| 752 |
+
# Initialize the request signer if not yet initialized after determining
|
| 753 |
+
# the current AWS region.
|
| 754 |
+
if self._request_signer is None:
|
| 755 |
+
self._region = self._aws_security_credentials_supplier.get_aws_region(
|
| 756 |
+
self._supplier_context, request
|
| 757 |
+
)
|
| 758 |
+
self._request_signer = RequestSigner(self._region)
|
| 759 |
+
|
| 760 |
+
# Retrieve the AWS security credentials needed to generate the signed
|
| 761 |
+
# request.
|
| 762 |
+
aws_security_credentials = self._aws_security_credentials_supplier.get_aws_security_credentials(
|
| 763 |
+
self._supplier_context, request
|
| 764 |
+
)
|
| 765 |
+
# Generate the signed request to AWS STS GetCallerIdentity API.
|
| 766 |
+
# Use the required regional endpoint. Otherwise, the request will fail.
|
| 767 |
+
request_options = self._request_signer.get_request_options(
|
| 768 |
+
aws_security_credentials,
|
| 769 |
+
self._cred_verification_url.replace("{region}", self._region),
|
| 770 |
+
"POST",
|
| 771 |
+
)
|
| 772 |
+
# The GCP STS endpoint expects the headers to be formatted as:
|
| 773 |
+
# [
|
| 774 |
+
# {key: 'x-amz-date', value: '...'},
|
| 775 |
+
# {key: 'Authorization', value: '...'},
|
| 776 |
+
# ...
|
| 777 |
+
# ]
|
| 778 |
+
# And then serialized as:
|
| 779 |
+
# quote(json.dumps({
|
| 780 |
+
# url: '...',
|
| 781 |
+
# method: 'POST',
|
| 782 |
+
# headers: [{key: 'x-amz-date', value: '...'}, ...]
|
| 783 |
+
# }))
|
| 784 |
+
request_headers = request_options.get("headers")
|
| 785 |
+
# The full, canonical resource name of the workload identity pool
|
| 786 |
+
# provider, with or without the HTTPS prefix.
|
| 787 |
+
# Including this header as part of the signature is recommended to
|
| 788 |
+
# ensure data integrity.
|
| 789 |
+
request_headers["x-goog-cloud-target-resource"] = self._target_resource
|
| 790 |
+
|
| 791 |
+
# Serialize AWS signed request.
|
| 792 |
+
aws_signed_req = {}
|
| 793 |
+
aws_signed_req["url"] = request_options.get("url")
|
| 794 |
+
aws_signed_req["method"] = request_options.get("method")
|
| 795 |
+
aws_signed_req["headers"] = []
|
| 796 |
+
# Reformat header to GCP STS expected format.
|
| 797 |
+
for key in request_headers.keys():
|
| 798 |
+
aws_signed_req["headers"].append(
|
| 799 |
+
{"key": key, "value": request_headers[key]}
|
| 800 |
+
)
|
| 801 |
+
|
| 802 |
+
return urllib.parse.quote(
|
| 803 |
+
json.dumps(aws_signed_req, separators=(",", ":"), sort_keys=True)
|
| 804 |
+
)
|
| 805 |
+
|
| 806 |
+
def _create_default_metrics_options(self):
|
| 807 |
+
metrics_options = super(Credentials, self)._create_default_metrics_options()
|
| 808 |
+
metrics_options["source"] = "aws"
|
| 809 |
+
if self._has_custom_supplier():
|
| 810 |
+
metrics_options["source"] = "programmatic"
|
| 811 |
+
return metrics_options
|
| 812 |
+
|
| 813 |
+
def _has_custom_supplier(self):
|
| 814 |
+
return self._credential_source is None
|
| 815 |
+
|
| 816 |
+
def _constructor_args(self):
|
| 817 |
+
args = super(Credentials, self)._constructor_args()
|
| 818 |
+
# If a custom supplier was used, append it to the args dict.
|
| 819 |
+
if self._has_custom_supplier():
|
| 820 |
+
args.update(
|
| 821 |
+
{
|
| 822 |
+
"aws_security_credentials_supplier": self._aws_security_credentials_supplier
|
| 823 |
+
}
|
| 824 |
+
)
|
| 825 |
+
return args
|
| 826 |
+
|
| 827 |
+
@classmethod
|
| 828 |
+
def from_info(cls, info, **kwargs):
|
| 829 |
+
"""Creates an AWS Credentials instance from parsed external account info.
|
| 830 |
+
|
| 831 |
+
Args:
|
| 832 |
+
info (Mapping[str, str]): The AWS external account info in Google
|
| 833 |
+
format.
|
| 834 |
+
kwargs: Additional arguments to pass to the constructor.
|
| 835 |
+
|
| 836 |
+
Returns:
|
| 837 |
+
google.auth.aws.Credentials: The constructed credentials.
|
| 838 |
+
|
| 839 |
+
Raises:
|
| 840 |
+
ValueError: For invalid parameters.
|
| 841 |
+
"""
|
| 842 |
+
aws_security_credentials_supplier = info.get(
|
| 843 |
+
"aws_security_credentials_supplier"
|
| 844 |
+
)
|
| 845 |
+
kwargs.update(
|
| 846 |
+
{"aws_security_credentials_supplier": aws_security_credentials_supplier}
|
| 847 |
+
)
|
| 848 |
+
return super(Credentials, cls).from_info(info, **kwargs)
|
| 849 |
+
|
| 850 |
+
@classmethod
|
| 851 |
+
def from_file(cls, filename, **kwargs):
|
| 852 |
+
"""Creates an AWS Credentials instance from an external account json file.
|
| 853 |
+
|
| 854 |
+
Args:
|
| 855 |
+
filename (str): The path to the AWS external account json file.
|
| 856 |
+
kwargs: Additional arguments to pass to the constructor.
|
| 857 |
+
|
| 858 |
+
Returns:
|
| 859 |
+
google.auth.aws.Credentials: The constructed credentials.
|
| 860 |
+
"""
|
| 861 |
+
return super(Credentials, cls).from_file(filename, **kwargs)
|
.venv/lib/python3.11/site-packages/google/auth/compute_engine/__pycache__/credentials.cpython-311.pyc
ADDED
|
Binary file (19.9 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/google/auth/downscoped.py
ADDED
|
@@ -0,0 +1,512 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2021 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""Downscoping with Credential Access Boundaries
|
| 16 |
+
|
| 17 |
+
This module provides the ability to downscope credentials using
|
| 18 |
+
`Downscoping with Credential Access Boundaries`_. This is useful to restrict the
|
| 19 |
+
Identity and Access Management (IAM) permissions that a short-lived credential
|
| 20 |
+
can use.
|
| 21 |
+
|
| 22 |
+
To downscope permissions of a source credential, a Credential Access Boundary
|
| 23 |
+
that specifies which resources the new credential can access, as well as
|
| 24 |
+
an upper bound on the permissions that are available on each resource, has to
|
| 25 |
+
be defined. A downscoped credential can then be instantiated using the source
|
| 26 |
+
credential and the Credential Access Boundary.
|
| 27 |
+
|
| 28 |
+
The common pattern of usage is to have a token broker with elevated access
|
| 29 |
+
generate these downscoped credentials from higher access source credentials and
|
| 30 |
+
pass the downscoped short-lived access tokens to a token consumer via some
|
| 31 |
+
secure authenticated channel for limited access to Google Cloud Storage
|
| 32 |
+
resources.
|
| 33 |
+
|
| 34 |
+
For example, a token broker can be set up on a server in a private network.
|
| 35 |
+
Various workloads (token consumers) in the same network will send authenticated
|
| 36 |
+
requests to that broker for downscoped tokens to access or modify specific google
|
| 37 |
+
cloud storage buckets.
|
| 38 |
+
|
| 39 |
+
The broker will instantiate downscoped credentials instances that can be used to
|
| 40 |
+
generate short lived downscoped access tokens that can be passed to the token
|
| 41 |
+
consumer. These downscoped access tokens can be injected by the consumer into
|
| 42 |
+
google.oauth2.Credentials and used to initialize a storage client instance to
|
| 43 |
+
access Google Cloud Storage resources with restricted access.
|
| 44 |
+
|
| 45 |
+
Note: Only Cloud Storage supports Credential Access Boundaries. Other Google
|
| 46 |
+
Cloud services do not support this feature.
|
| 47 |
+
|
| 48 |
+
.. _Downscoping with Credential Access Boundaries: https://cloud.google.com/iam/docs/downscoping-short-lived-credentials
|
| 49 |
+
"""
|
| 50 |
+
|
| 51 |
+
import datetime
|
| 52 |
+
|
| 53 |
+
from google.auth import _helpers
|
| 54 |
+
from google.auth import credentials
|
| 55 |
+
from google.auth import exceptions
|
| 56 |
+
from google.oauth2 import sts
|
| 57 |
+
|
| 58 |
+
# The maximum number of access boundary rules a Credential Access Boundary can
|
| 59 |
+
# contain.
|
| 60 |
+
_MAX_ACCESS_BOUNDARY_RULES_COUNT = 10
|
| 61 |
+
# The token exchange grant_type used for exchanging credentials.
|
| 62 |
+
_STS_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange"
|
| 63 |
+
# The token exchange requested_token_type. This is always an access_token.
|
| 64 |
+
_STS_REQUESTED_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token"
|
| 65 |
+
# The STS token URL used to exchanged a short lived access token for a downscoped one.
|
| 66 |
+
_STS_TOKEN_URL_PATTERN = "https://sts.{}/v1/token"
|
| 67 |
+
# The subject token type to use when exchanging a short lived access token for a
|
| 68 |
+
# downscoped token.
|
| 69 |
+
_STS_SUBJECT_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token"
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
class CredentialAccessBoundary(object):
|
| 73 |
+
"""Defines a Credential Access Boundary which contains a list of access boundary
|
| 74 |
+
rules. Each rule contains information on the resource that the rule applies to,
|
| 75 |
+
the upper bound of the permissions that are available on that resource and an
|
| 76 |
+
optional condition to further restrict permissions.
|
| 77 |
+
"""
|
| 78 |
+
|
| 79 |
+
def __init__(self, rules=[]):
|
| 80 |
+
"""Instantiates a Credential Access Boundary. A Credential Access Boundary
|
| 81 |
+
can contain up to 10 access boundary rules.
|
| 82 |
+
|
| 83 |
+
Args:
|
| 84 |
+
rules (Sequence[google.auth.downscoped.AccessBoundaryRule]): The list of
|
| 85 |
+
access boundary rules limiting the access that a downscoped credential
|
| 86 |
+
will have.
|
| 87 |
+
Raises:
|
| 88 |
+
InvalidType: If any of the rules are not a valid type.
|
| 89 |
+
InvalidValue: If the provided rules exceed the maximum allowed.
|
| 90 |
+
"""
|
| 91 |
+
self.rules = rules
|
| 92 |
+
|
| 93 |
+
@property
|
| 94 |
+
def rules(self):
|
| 95 |
+
"""Returns the list of access boundary rules defined on the Credential
|
| 96 |
+
Access Boundary.
|
| 97 |
+
|
| 98 |
+
Returns:
|
| 99 |
+
Tuple[google.auth.downscoped.AccessBoundaryRule, ...]: The list of access
|
| 100 |
+
boundary rules defined on the Credential Access Boundary. These are returned
|
| 101 |
+
as an immutable tuple to prevent modification.
|
| 102 |
+
"""
|
| 103 |
+
return tuple(self._rules)
|
| 104 |
+
|
| 105 |
+
@rules.setter
|
| 106 |
+
def rules(self, value):
|
| 107 |
+
"""Updates the current rules on the Credential Access Boundary. This will overwrite
|
| 108 |
+
the existing set of rules.
|
| 109 |
+
|
| 110 |
+
Args:
|
| 111 |
+
value (Sequence[google.auth.downscoped.AccessBoundaryRule]): The list of
|
| 112 |
+
access boundary rules limiting the access that a downscoped credential
|
| 113 |
+
will have.
|
| 114 |
+
Raises:
|
| 115 |
+
InvalidType: If any of the rules are not a valid type.
|
| 116 |
+
InvalidValue: If the provided rules exceed the maximum allowed.
|
| 117 |
+
"""
|
| 118 |
+
if len(value) > _MAX_ACCESS_BOUNDARY_RULES_COUNT:
|
| 119 |
+
raise exceptions.InvalidValue(
|
| 120 |
+
"Credential access boundary rules can have a maximum of {} rules.".format(
|
| 121 |
+
_MAX_ACCESS_BOUNDARY_RULES_COUNT
|
| 122 |
+
)
|
| 123 |
+
)
|
| 124 |
+
for access_boundary_rule in value:
|
| 125 |
+
if not isinstance(access_boundary_rule, AccessBoundaryRule):
|
| 126 |
+
raise exceptions.InvalidType(
|
| 127 |
+
"List of rules provided do not contain a valid 'google.auth.downscoped.AccessBoundaryRule'."
|
| 128 |
+
)
|
| 129 |
+
# Make a copy of the original list.
|
| 130 |
+
self._rules = list(value)
|
| 131 |
+
|
| 132 |
+
def add_rule(self, rule):
|
| 133 |
+
"""Adds a single access boundary rule to the existing rules.
|
| 134 |
+
|
| 135 |
+
Args:
|
| 136 |
+
rule (google.auth.downscoped.AccessBoundaryRule): The access boundary rule,
|
| 137 |
+
limiting the access that a downscoped credential will have, to be added to
|
| 138 |
+
the existing rules.
|
| 139 |
+
Raises:
|
| 140 |
+
InvalidType: If any of the rules are not a valid type.
|
| 141 |
+
InvalidValue: If the provided rules exceed the maximum allowed.
|
| 142 |
+
"""
|
| 143 |
+
if len(self.rules) == _MAX_ACCESS_BOUNDARY_RULES_COUNT:
|
| 144 |
+
raise exceptions.InvalidValue(
|
| 145 |
+
"Credential access boundary rules can have a maximum of {} rules.".format(
|
| 146 |
+
_MAX_ACCESS_BOUNDARY_RULES_COUNT
|
| 147 |
+
)
|
| 148 |
+
)
|
| 149 |
+
if not isinstance(rule, AccessBoundaryRule):
|
| 150 |
+
raise exceptions.InvalidType(
|
| 151 |
+
"The provided rule does not contain a valid 'google.auth.downscoped.AccessBoundaryRule'."
|
| 152 |
+
)
|
| 153 |
+
self._rules.append(rule)
|
| 154 |
+
|
| 155 |
+
def to_json(self):
|
| 156 |
+
"""Generates the dictionary representation of the Credential Access Boundary.
|
| 157 |
+
This uses the format expected by the Security Token Service API as documented in
|
| 158 |
+
`Defining a Credential Access Boundary`_.
|
| 159 |
+
|
| 160 |
+
.. _Defining a Credential Access Boundary:
|
| 161 |
+
https://cloud.google.com/iam/docs/downscoping-short-lived-credentials#define-boundary
|
| 162 |
+
|
| 163 |
+
Returns:
|
| 164 |
+
Mapping: Credential Access Boundary Rule represented in a dictionary object.
|
| 165 |
+
"""
|
| 166 |
+
rules = []
|
| 167 |
+
for access_boundary_rule in self.rules:
|
| 168 |
+
rules.append(access_boundary_rule.to_json())
|
| 169 |
+
|
| 170 |
+
return {"accessBoundary": {"accessBoundaryRules": rules}}
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
class AccessBoundaryRule(object):
|
| 174 |
+
"""Defines an access boundary rule which contains information on the resource that
|
| 175 |
+
the rule applies to, the upper bound of the permissions that are available on that
|
| 176 |
+
resource and an optional condition to further restrict permissions.
|
| 177 |
+
"""
|
| 178 |
+
|
| 179 |
+
def __init__(
|
| 180 |
+
self, available_resource, available_permissions, availability_condition=None
|
| 181 |
+
):
|
| 182 |
+
"""Instantiates a single access boundary rule.
|
| 183 |
+
|
| 184 |
+
Args:
|
| 185 |
+
available_resource (str): The full resource name of the Cloud Storage bucket
|
| 186 |
+
that the rule applies to. Use the format
|
| 187 |
+
"//storage.googleapis.com/projects/_/buckets/bucket-name".
|
| 188 |
+
available_permissions (Sequence[str]): A list defining the upper bound that
|
| 189 |
+
the downscoped token will have on the available permissions for the
|
| 190 |
+
resource. Each value is the identifier for an IAM predefined role or
|
| 191 |
+
custom role, with the prefix "inRole:". For example:
|
| 192 |
+
"inRole:roles/storage.objectViewer".
|
| 193 |
+
Only the permissions in these roles will be available.
|
| 194 |
+
availability_condition (Optional[google.auth.downscoped.AvailabilityCondition]):
|
| 195 |
+
Optional condition that restricts the availability of permissions to
|
| 196 |
+
specific Cloud Storage objects.
|
| 197 |
+
|
| 198 |
+
Raises:
|
| 199 |
+
InvalidType: If any of the parameters are not of the expected types.
|
| 200 |
+
InvalidValue: If any of the parameters are not of the expected values.
|
| 201 |
+
"""
|
| 202 |
+
self.available_resource = available_resource
|
| 203 |
+
self.available_permissions = available_permissions
|
| 204 |
+
self.availability_condition = availability_condition
|
| 205 |
+
|
| 206 |
+
@property
|
| 207 |
+
def available_resource(self):
|
| 208 |
+
"""Returns the current available resource.
|
| 209 |
+
|
| 210 |
+
Returns:
|
| 211 |
+
str: The current available resource.
|
| 212 |
+
"""
|
| 213 |
+
return self._available_resource
|
| 214 |
+
|
| 215 |
+
@available_resource.setter
|
| 216 |
+
def available_resource(self, value):
|
| 217 |
+
"""Updates the current available resource.
|
| 218 |
+
|
| 219 |
+
Args:
|
| 220 |
+
value (str): The updated value of the available resource.
|
| 221 |
+
|
| 222 |
+
Raises:
|
| 223 |
+
google.auth.exceptions.InvalidType: If the value is not a string.
|
| 224 |
+
"""
|
| 225 |
+
if not isinstance(value, str):
|
| 226 |
+
raise exceptions.InvalidType(
|
| 227 |
+
"The provided available_resource is not a string."
|
| 228 |
+
)
|
| 229 |
+
self._available_resource = value
|
| 230 |
+
|
| 231 |
+
@property
|
| 232 |
+
def available_permissions(self):
|
| 233 |
+
"""Returns the current available permissions.
|
| 234 |
+
|
| 235 |
+
Returns:
|
| 236 |
+
Tuple[str, ...]: The current available permissions. These are returned
|
| 237 |
+
as an immutable tuple to prevent modification.
|
| 238 |
+
"""
|
| 239 |
+
return tuple(self._available_permissions)
|
| 240 |
+
|
| 241 |
+
@available_permissions.setter
|
| 242 |
+
def available_permissions(self, value):
|
| 243 |
+
"""Updates the current available permissions.
|
| 244 |
+
|
| 245 |
+
Args:
|
| 246 |
+
value (Sequence[str]): The updated value of the available permissions.
|
| 247 |
+
|
| 248 |
+
Raises:
|
| 249 |
+
InvalidType: If the value is not a list of strings.
|
| 250 |
+
InvalidValue: If the value is not valid.
|
| 251 |
+
"""
|
| 252 |
+
for available_permission in value:
|
| 253 |
+
if not isinstance(available_permission, str):
|
| 254 |
+
raise exceptions.InvalidType(
|
| 255 |
+
"Provided available_permissions are not a list of strings."
|
| 256 |
+
)
|
| 257 |
+
if available_permission.find("inRole:") != 0:
|
| 258 |
+
raise exceptions.InvalidValue(
|
| 259 |
+
"available_permissions must be prefixed with 'inRole:'."
|
| 260 |
+
)
|
| 261 |
+
# Make a copy of the original list.
|
| 262 |
+
self._available_permissions = list(value)
|
| 263 |
+
|
| 264 |
+
@property
|
| 265 |
+
def availability_condition(self):
|
| 266 |
+
"""Returns the current availability condition.
|
| 267 |
+
|
| 268 |
+
Returns:
|
| 269 |
+
Optional[google.auth.downscoped.AvailabilityCondition]: The current
|
| 270 |
+
availability condition.
|
| 271 |
+
"""
|
| 272 |
+
return self._availability_condition
|
| 273 |
+
|
| 274 |
+
@availability_condition.setter
|
| 275 |
+
def availability_condition(self, value):
|
| 276 |
+
"""Updates the current availability condition.
|
| 277 |
+
|
| 278 |
+
Args:
|
| 279 |
+
value (Optional[google.auth.downscoped.AvailabilityCondition]): The updated
|
| 280 |
+
value of the availability condition.
|
| 281 |
+
|
| 282 |
+
Raises:
|
| 283 |
+
google.auth.exceptions.InvalidType: If the value is not of type google.auth.downscoped.AvailabilityCondition
|
| 284 |
+
or None.
|
| 285 |
+
"""
|
| 286 |
+
if not isinstance(value, AvailabilityCondition) and value is not None:
|
| 287 |
+
raise exceptions.InvalidType(
|
| 288 |
+
"The provided availability_condition is not a 'google.auth.downscoped.AvailabilityCondition' or None."
|
| 289 |
+
)
|
| 290 |
+
self._availability_condition = value
|
| 291 |
+
|
| 292 |
+
def to_json(self):
|
| 293 |
+
"""Generates the dictionary representation of the access boundary rule.
|
| 294 |
+
This uses the format expected by the Security Token Service API as documented in
|
| 295 |
+
`Defining a Credential Access Boundary`_.
|
| 296 |
+
|
| 297 |
+
.. _Defining a Credential Access Boundary:
|
| 298 |
+
https://cloud.google.com/iam/docs/downscoping-short-lived-credentials#define-boundary
|
| 299 |
+
|
| 300 |
+
Returns:
|
| 301 |
+
Mapping: The access boundary rule represented in a dictionary object.
|
| 302 |
+
"""
|
| 303 |
+
json = {
|
| 304 |
+
"availablePermissions": list(self.available_permissions),
|
| 305 |
+
"availableResource": self.available_resource,
|
| 306 |
+
}
|
| 307 |
+
if self.availability_condition:
|
| 308 |
+
json["availabilityCondition"] = self.availability_condition.to_json()
|
| 309 |
+
return json
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
class AvailabilityCondition(object):
|
| 313 |
+
"""An optional condition that can be used as part of a Credential Access Boundary
|
| 314 |
+
to further restrict permissions."""
|
| 315 |
+
|
| 316 |
+
def __init__(self, expression, title=None, description=None):
|
| 317 |
+
"""Instantiates an availability condition using the provided expression and
|
| 318 |
+
optional title or description.
|
| 319 |
+
|
| 320 |
+
Args:
|
| 321 |
+
expression (str): A condition expression that specifies the Cloud Storage
|
| 322 |
+
objects where permissions are available. For example, this expression
|
| 323 |
+
makes permissions available for objects whose name starts with "customer-a":
|
| 324 |
+
"resource.name.startsWith('projects/_/buckets/example-bucket/objects/customer-a')"
|
| 325 |
+
title (Optional[str]): An optional short string that identifies the purpose of
|
| 326 |
+
the condition.
|
| 327 |
+
description (Optional[str]): Optional details about the purpose of the condition.
|
| 328 |
+
|
| 329 |
+
Raises:
|
| 330 |
+
InvalidType: If any of the parameters are not of the expected types.
|
| 331 |
+
InvalidValue: If any of the parameters are not of the expected values.
|
| 332 |
+
"""
|
| 333 |
+
self.expression = expression
|
| 334 |
+
self.title = title
|
| 335 |
+
self.description = description
|
| 336 |
+
|
| 337 |
+
@property
|
| 338 |
+
def expression(self):
|
| 339 |
+
"""Returns the current condition expression.
|
| 340 |
+
|
| 341 |
+
Returns:
|
| 342 |
+
str: The current conditon expression.
|
| 343 |
+
"""
|
| 344 |
+
return self._expression
|
| 345 |
+
|
| 346 |
+
@expression.setter
|
| 347 |
+
def expression(self, value):
|
| 348 |
+
"""Updates the current condition expression.
|
| 349 |
+
|
| 350 |
+
Args:
|
| 351 |
+
value (str): The updated value of the condition expression.
|
| 352 |
+
|
| 353 |
+
Raises:
|
| 354 |
+
google.auth.exceptions.InvalidType: If the value is not of type string.
|
| 355 |
+
"""
|
| 356 |
+
if not isinstance(value, str):
|
| 357 |
+
raise exceptions.InvalidType("The provided expression is not a string.")
|
| 358 |
+
self._expression = value
|
| 359 |
+
|
| 360 |
+
@property
|
| 361 |
+
def title(self):
|
| 362 |
+
"""Returns the current title.
|
| 363 |
+
|
| 364 |
+
Returns:
|
| 365 |
+
Optional[str]: The current title.
|
| 366 |
+
"""
|
| 367 |
+
return self._title
|
| 368 |
+
|
| 369 |
+
@title.setter
|
| 370 |
+
def title(self, value):
|
| 371 |
+
"""Updates the current title.
|
| 372 |
+
|
| 373 |
+
Args:
|
| 374 |
+
value (Optional[str]): The updated value of the title.
|
| 375 |
+
|
| 376 |
+
Raises:
|
| 377 |
+
google.auth.exceptions.InvalidType: If the value is not of type string or None.
|
| 378 |
+
"""
|
| 379 |
+
if not isinstance(value, str) and value is not None:
|
| 380 |
+
raise exceptions.InvalidType("The provided title is not a string or None.")
|
| 381 |
+
self._title = value
|
| 382 |
+
|
| 383 |
+
@property
|
| 384 |
+
def description(self):
|
| 385 |
+
"""Returns the current description.
|
| 386 |
+
|
| 387 |
+
Returns:
|
| 388 |
+
Optional[str]: The current description.
|
| 389 |
+
"""
|
| 390 |
+
return self._description
|
| 391 |
+
|
| 392 |
+
@description.setter
|
| 393 |
+
def description(self, value):
|
| 394 |
+
"""Updates the current description.
|
| 395 |
+
|
| 396 |
+
Args:
|
| 397 |
+
value (Optional[str]): The updated value of the description.
|
| 398 |
+
|
| 399 |
+
Raises:
|
| 400 |
+
google.auth.exceptions.InvalidType: If the value is not of type string or None.
|
| 401 |
+
"""
|
| 402 |
+
if not isinstance(value, str) and value is not None:
|
| 403 |
+
raise exceptions.InvalidType(
|
| 404 |
+
"The provided description is not a string or None."
|
| 405 |
+
)
|
| 406 |
+
self._description = value
|
| 407 |
+
|
| 408 |
+
def to_json(self):
|
| 409 |
+
"""Generates the dictionary representation of the availability condition.
|
| 410 |
+
This uses the format expected by the Security Token Service API as documented in
|
| 411 |
+
`Defining a Credential Access Boundary`_.
|
| 412 |
+
|
| 413 |
+
.. _Defining a Credential Access Boundary:
|
| 414 |
+
https://cloud.google.com/iam/docs/downscoping-short-lived-credentials#define-boundary
|
| 415 |
+
|
| 416 |
+
Returns:
|
| 417 |
+
Mapping[str, str]: The availability condition represented in a dictionary
|
| 418 |
+
object.
|
| 419 |
+
"""
|
| 420 |
+
json = {"expression": self.expression}
|
| 421 |
+
if self.title:
|
| 422 |
+
json["title"] = self.title
|
| 423 |
+
if self.description:
|
| 424 |
+
json["description"] = self.description
|
| 425 |
+
return json
|
| 426 |
+
|
| 427 |
+
|
| 428 |
+
class Credentials(credentials.CredentialsWithQuotaProject):
|
| 429 |
+
"""Defines a set of Google credentials that are downscoped from an existing set
|
| 430 |
+
of Google OAuth2 credentials. This is useful to restrict the Identity and Access
|
| 431 |
+
Management (IAM) permissions that a short-lived credential can use.
|
| 432 |
+
The common pattern of usage is to have a token broker with elevated access
|
| 433 |
+
generate these downscoped credentials from higher access source credentials and
|
| 434 |
+
pass the downscoped short-lived access tokens to a token consumer via some
|
| 435 |
+
secure authenticated channel for limited access to Google Cloud Storage
|
| 436 |
+
resources.
|
| 437 |
+
"""
|
| 438 |
+
|
| 439 |
+
def __init__(
|
| 440 |
+
self,
|
| 441 |
+
source_credentials,
|
| 442 |
+
credential_access_boundary,
|
| 443 |
+
quota_project_id=None,
|
| 444 |
+
universe_domain=credentials.DEFAULT_UNIVERSE_DOMAIN,
|
| 445 |
+
):
|
| 446 |
+
"""Instantiates a downscoped credentials object using the provided source
|
| 447 |
+
credentials and credential access boundary rules.
|
| 448 |
+
To downscope permissions of a source credential, a Credential Access Boundary
|
| 449 |
+
that specifies which resources the new credential can access, as well as an
|
| 450 |
+
upper bound on the permissions that are available on each resource, has to be
|
| 451 |
+
defined. A downscoped credential can then be instantiated using the source
|
| 452 |
+
credential and the Credential Access Boundary.
|
| 453 |
+
|
| 454 |
+
Args:
|
| 455 |
+
source_credentials (google.auth.credentials.Credentials): The source credentials
|
| 456 |
+
to be downscoped based on the provided Credential Access Boundary rules.
|
| 457 |
+
credential_access_boundary (google.auth.downscoped.CredentialAccessBoundary):
|
| 458 |
+
The Credential Access Boundary which contains a list of access boundary
|
| 459 |
+
rules. Each rule contains information on the resource that the rule applies to,
|
| 460 |
+
the upper bound of the permissions that are available on that resource and an
|
| 461 |
+
optional condition to further restrict permissions.
|
| 462 |
+
quota_project_id (Optional[str]): The optional quota project ID.
|
| 463 |
+
universe_domain (Optional[str]): The universe domain value, default is googleapis.com
|
| 464 |
+
Raises:
|
| 465 |
+
google.auth.exceptions.RefreshError: If the source credentials
|
| 466 |
+
return an error on token refresh.
|
| 467 |
+
google.auth.exceptions.OAuthError: If the STS token exchange
|
| 468 |
+
endpoint returned an error during downscoped token generation.
|
| 469 |
+
"""
|
| 470 |
+
|
| 471 |
+
super(Credentials, self).__init__()
|
| 472 |
+
self._source_credentials = source_credentials
|
| 473 |
+
self._credential_access_boundary = credential_access_boundary
|
| 474 |
+
self._quota_project_id = quota_project_id
|
| 475 |
+
self._universe_domain = universe_domain or credentials.DEFAULT_UNIVERSE_DOMAIN
|
| 476 |
+
self._sts_client = sts.Client(
|
| 477 |
+
_STS_TOKEN_URL_PATTERN.format(self.universe_domain)
|
| 478 |
+
)
|
| 479 |
+
|
| 480 |
+
@_helpers.copy_docstring(credentials.Credentials)
|
| 481 |
+
def refresh(self, request):
|
| 482 |
+
# Generate an access token from the source credentials.
|
| 483 |
+
self._source_credentials.refresh(request)
|
| 484 |
+
now = _helpers.utcnow()
|
| 485 |
+
# Exchange the access token for a downscoped access token.
|
| 486 |
+
response_data = self._sts_client.exchange_token(
|
| 487 |
+
request=request,
|
| 488 |
+
grant_type=_STS_GRANT_TYPE,
|
| 489 |
+
subject_token=self._source_credentials.token,
|
| 490 |
+
subject_token_type=_STS_SUBJECT_TOKEN_TYPE,
|
| 491 |
+
requested_token_type=_STS_REQUESTED_TOKEN_TYPE,
|
| 492 |
+
additional_options=self._credential_access_boundary.to_json(),
|
| 493 |
+
)
|
| 494 |
+
self.token = response_data.get("access_token")
|
| 495 |
+
# For downscoping CAB flow, the STS endpoint may not return the expiration
|
| 496 |
+
# field for some flows. The generated downscoped token should always have
|
| 497 |
+
# the same expiration time as the source credentials. When no expires_in
|
| 498 |
+
# field is returned in the response, we can just get the expiration time
|
| 499 |
+
# from the source credentials.
|
| 500 |
+
if response_data.get("expires_in"):
|
| 501 |
+
lifetime = datetime.timedelta(seconds=response_data.get("expires_in"))
|
| 502 |
+
self.expiry = now + lifetime
|
| 503 |
+
else:
|
| 504 |
+
self.expiry = self._source_credentials.expiry
|
| 505 |
+
|
| 506 |
+
@_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
|
| 507 |
+
def with_quota_project(self, quota_project_id):
|
| 508 |
+
return self.__class__(
|
| 509 |
+
self._source_credentials,
|
| 510 |
+
self._credential_access_boundary,
|
| 511 |
+
quota_project_id=quota_project_id,
|
| 512 |
+
)
|
.venv/lib/python3.11/site-packages/google/auth/external_account_authorized_user.py
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2022 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""External Account Authorized User Credentials.
|
| 16 |
+
This module provides credentials based on OAuth 2.0 access and refresh tokens.
|
| 17 |
+
These credentials usually access resources on behalf of a user (resource
|
| 18 |
+
owner).
|
| 19 |
+
|
| 20 |
+
Specifically, these are sourced using external identities via Workforce Identity Federation.
|
| 21 |
+
|
| 22 |
+
Obtaining the initial access and refresh token can be done through the Google Cloud CLI.
|
| 23 |
+
|
| 24 |
+
Example credential:
|
| 25 |
+
{
|
| 26 |
+
"type": "external_account_authorized_user",
|
| 27 |
+
"audience": "//iam.googleapis.com/locations/global/workforcePools/$WORKFORCE_POOL_ID/providers/$PROVIDER_ID",
|
| 28 |
+
"refresh_token": "refreshToken",
|
| 29 |
+
"token_url": "https://sts.googleapis.com/v1/oauth/token",
|
| 30 |
+
"token_info_url": "https://sts.googleapis.com/v1/instrospect",
|
| 31 |
+
"client_id": "clientId",
|
| 32 |
+
"client_secret": "clientSecret"
|
| 33 |
+
}
|
| 34 |
+
"""
|
| 35 |
+
|
| 36 |
+
import datetime
|
| 37 |
+
import io
|
| 38 |
+
import json
|
| 39 |
+
|
| 40 |
+
from google.auth import _helpers
|
| 41 |
+
from google.auth import credentials
|
| 42 |
+
from google.auth import exceptions
|
| 43 |
+
from google.oauth2 import sts
|
| 44 |
+
from google.oauth2 import utils
|
| 45 |
+
|
| 46 |
+
_EXTERNAL_ACCOUNT_AUTHORIZED_USER_JSON_TYPE = "external_account_authorized_user"
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class Credentials(
|
| 50 |
+
credentials.CredentialsWithQuotaProject,
|
| 51 |
+
credentials.ReadOnlyScoped,
|
| 52 |
+
credentials.CredentialsWithTokenUri,
|
| 53 |
+
):
|
| 54 |
+
"""Credentials for External Account Authorized Users.
|
| 55 |
+
|
| 56 |
+
This is used to instantiate Credentials for exchanging refresh tokens from
|
| 57 |
+
authorized users for Google access token and authorizing requests to Google
|
| 58 |
+
APIs.
|
| 59 |
+
|
| 60 |
+
The credentials are considered immutable. If you want to modify the
|
| 61 |
+
quota project, use `with_quota_project` and if you want to modify the token
|
| 62 |
+
uri, use `with_token_uri`.
|
| 63 |
+
"""
|
| 64 |
+
|
| 65 |
+
def __init__(
|
| 66 |
+
self,
|
| 67 |
+
token=None,
|
| 68 |
+
expiry=None,
|
| 69 |
+
refresh_token=None,
|
| 70 |
+
audience=None,
|
| 71 |
+
client_id=None,
|
| 72 |
+
client_secret=None,
|
| 73 |
+
token_url=None,
|
| 74 |
+
token_info_url=None,
|
| 75 |
+
revoke_url=None,
|
| 76 |
+
scopes=None,
|
| 77 |
+
quota_project_id=None,
|
| 78 |
+
universe_domain=credentials.DEFAULT_UNIVERSE_DOMAIN,
|
| 79 |
+
):
|
| 80 |
+
"""Instantiates a external account authorized user credentials object.
|
| 81 |
+
|
| 82 |
+
Args:
|
| 83 |
+
token (str): The OAuth 2.0 access token. Can be None if refresh information
|
| 84 |
+
is provided.
|
| 85 |
+
expiry (datetime.datetime): The optional expiration datetime of the OAuth 2.0 access
|
| 86 |
+
token.
|
| 87 |
+
refresh_token (str): The optional OAuth 2.0 refresh token. If specified,
|
| 88 |
+
credentials can be refreshed.
|
| 89 |
+
audience (str): The optional STS audience which contains the resource name for the workforce
|
| 90 |
+
pool and the provider identifier in that pool.
|
| 91 |
+
client_id (str): The OAuth 2.0 client ID. Must be specified for refresh, can be left as
|
| 92 |
+
None if the token can not be refreshed.
|
| 93 |
+
client_secret (str): The OAuth 2.0 client secret. Must be specified for refresh, can be
|
| 94 |
+
left as None if the token can not be refreshed.
|
| 95 |
+
token_url (str): The optional STS token exchange endpoint for refresh. Must be specified for
|
| 96 |
+
refresh, can be left as None if the token can not be refreshed.
|
| 97 |
+
token_info_url (str): The optional STS endpoint URL for token introspection.
|
| 98 |
+
revoke_url (str): The optional STS endpoint URL for revoking tokens.
|
| 99 |
+
quota_project_id (str): The optional project ID used for quota and billing.
|
| 100 |
+
This project may be different from the project used to
|
| 101 |
+
create the credentials.
|
| 102 |
+
universe_domain (Optional[str]): The universe domain. The default value
|
| 103 |
+
is googleapis.com.
|
| 104 |
+
|
| 105 |
+
Returns:
|
| 106 |
+
google.auth.external_account_authorized_user.Credentials: The
|
| 107 |
+
constructed credentials.
|
| 108 |
+
"""
|
| 109 |
+
super(Credentials, self).__init__()
|
| 110 |
+
|
| 111 |
+
self.token = token
|
| 112 |
+
self.expiry = expiry
|
| 113 |
+
self._audience = audience
|
| 114 |
+
self._refresh_token = refresh_token
|
| 115 |
+
self._token_url = token_url
|
| 116 |
+
self._token_info_url = token_info_url
|
| 117 |
+
self._client_id = client_id
|
| 118 |
+
self._client_secret = client_secret
|
| 119 |
+
self._revoke_url = revoke_url
|
| 120 |
+
self._quota_project_id = quota_project_id
|
| 121 |
+
self._scopes = scopes
|
| 122 |
+
self._universe_domain = universe_domain or credentials.DEFAULT_UNIVERSE_DOMAIN
|
| 123 |
+
self._cred_file_path = None
|
| 124 |
+
|
| 125 |
+
if not self.valid and not self.can_refresh:
|
| 126 |
+
raise exceptions.InvalidOperation(
|
| 127 |
+
"Token should be created with fields to make it valid (`token` and "
|
| 128 |
+
"`expiry`), or fields to allow it to refresh (`refresh_token`, "
|
| 129 |
+
"`token_url`, `client_id`, `client_secret`)."
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
self._client_auth = None
|
| 133 |
+
if self._client_id:
|
| 134 |
+
self._client_auth = utils.ClientAuthentication(
|
| 135 |
+
utils.ClientAuthType.basic, self._client_id, self._client_secret
|
| 136 |
+
)
|
| 137 |
+
self._sts_client = sts.Client(self._token_url, self._client_auth)
|
| 138 |
+
|
| 139 |
+
@property
|
| 140 |
+
def info(self):
|
| 141 |
+
"""Generates the serializable dictionary representation of the current
|
| 142 |
+
credentials.
|
| 143 |
+
|
| 144 |
+
Returns:
|
| 145 |
+
Mapping: The dictionary representation of the credentials. This is the
|
| 146 |
+
reverse of the "from_info" method defined in this class. It is
|
| 147 |
+
useful for serializing the current credentials so it can deserialized
|
| 148 |
+
later.
|
| 149 |
+
"""
|
| 150 |
+
config_info = self.constructor_args()
|
| 151 |
+
config_info.update(type=_EXTERNAL_ACCOUNT_AUTHORIZED_USER_JSON_TYPE)
|
| 152 |
+
if config_info["expiry"]:
|
| 153 |
+
config_info["expiry"] = config_info["expiry"].isoformat() + "Z"
|
| 154 |
+
|
| 155 |
+
return {key: value for key, value in config_info.items() if value is not None}
|
| 156 |
+
|
| 157 |
+
def constructor_args(self):
|
| 158 |
+
return {
|
| 159 |
+
"audience": self._audience,
|
| 160 |
+
"refresh_token": self._refresh_token,
|
| 161 |
+
"token_url": self._token_url,
|
| 162 |
+
"token_info_url": self._token_info_url,
|
| 163 |
+
"client_id": self._client_id,
|
| 164 |
+
"client_secret": self._client_secret,
|
| 165 |
+
"token": self.token,
|
| 166 |
+
"expiry": self.expiry,
|
| 167 |
+
"revoke_url": self._revoke_url,
|
| 168 |
+
"scopes": self._scopes,
|
| 169 |
+
"quota_project_id": self._quota_project_id,
|
| 170 |
+
"universe_domain": self._universe_domain,
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
@property
|
| 174 |
+
def scopes(self):
|
| 175 |
+
"""Optional[str]: The OAuth 2.0 permission scopes."""
|
| 176 |
+
return self._scopes
|
| 177 |
+
|
| 178 |
+
@property
|
| 179 |
+
def requires_scopes(self):
|
| 180 |
+
""" False: OAuth 2.0 credentials have their scopes set when
|
| 181 |
+
the initial token is requested and can not be changed."""
|
| 182 |
+
return False
|
| 183 |
+
|
| 184 |
+
@property
|
| 185 |
+
def client_id(self):
|
| 186 |
+
"""Optional[str]: The OAuth 2.0 client ID."""
|
| 187 |
+
return self._client_id
|
| 188 |
+
|
| 189 |
+
@property
|
| 190 |
+
def client_secret(self):
|
| 191 |
+
"""Optional[str]: The OAuth 2.0 client secret."""
|
| 192 |
+
return self._client_secret
|
| 193 |
+
|
| 194 |
+
@property
|
| 195 |
+
def audience(self):
|
| 196 |
+
"""Optional[str]: The STS audience which contains the resource name for the
|
| 197 |
+
workforce pool and the provider identifier in that pool."""
|
| 198 |
+
return self._audience
|
| 199 |
+
|
| 200 |
+
@property
|
| 201 |
+
def refresh_token(self):
|
| 202 |
+
"""Optional[str]: The OAuth 2.0 refresh token."""
|
| 203 |
+
return self._refresh_token
|
| 204 |
+
|
| 205 |
+
@property
|
| 206 |
+
def token_url(self):
|
| 207 |
+
"""Optional[str]: The STS token exchange endpoint for refresh."""
|
| 208 |
+
return self._token_url
|
| 209 |
+
|
| 210 |
+
@property
|
| 211 |
+
def token_info_url(self):
|
| 212 |
+
"""Optional[str]: The STS endpoint for token info."""
|
| 213 |
+
return self._token_info_url
|
| 214 |
+
|
| 215 |
+
@property
|
| 216 |
+
def revoke_url(self):
|
| 217 |
+
"""Optional[str]: The STS endpoint for token revocation."""
|
| 218 |
+
return self._revoke_url
|
| 219 |
+
|
| 220 |
+
@property
|
| 221 |
+
def is_user(self):
|
| 222 |
+
""" True: This credential always represents a user."""
|
| 223 |
+
return True
|
| 224 |
+
|
| 225 |
+
@property
|
| 226 |
+
def can_refresh(self):
|
| 227 |
+
return all(
|
| 228 |
+
(self._refresh_token, self._token_url, self._client_id, self._client_secret)
|
| 229 |
+
)
|
| 230 |
+
|
| 231 |
+
def get_project_id(self, request=None):
|
| 232 |
+
"""Retrieves the project ID corresponding to the workload identity or workforce pool.
|
| 233 |
+
For workforce pool credentials, it returns the project ID corresponding to
|
| 234 |
+
the workforce_pool_user_project.
|
| 235 |
+
|
| 236 |
+
When not determinable, None is returned.
|
| 237 |
+
|
| 238 |
+
Args:
|
| 239 |
+
request (google.auth.transport.requests.Request): Request object.
|
| 240 |
+
Unused here, but passed from _default.default().
|
| 241 |
+
|
| 242 |
+
Return:
|
| 243 |
+
str: project ID is not determinable for this credential type so it returns None
|
| 244 |
+
"""
|
| 245 |
+
|
| 246 |
+
return None
|
| 247 |
+
|
| 248 |
+
def to_json(self, strip=None):
|
| 249 |
+
"""Utility function that creates a JSON representation of this
|
| 250 |
+
credential.
|
| 251 |
+
Args:
|
| 252 |
+
strip (Sequence[str]): Optional list of members to exclude from the
|
| 253 |
+
generated JSON.
|
| 254 |
+
Returns:
|
| 255 |
+
str: A JSON representation of this instance. When converted into
|
| 256 |
+
a dictionary, it can be passed to from_info()
|
| 257 |
+
to create a new instance.
|
| 258 |
+
"""
|
| 259 |
+
strip = strip if strip else []
|
| 260 |
+
return json.dumps({k: v for (k, v) in self.info.items() if k not in strip})
|
| 261 |
+
|
| 262 |
+
def refresh(self, request):
|
| 263 |
+
"""Refreshes the access token.
|
| 264 |
+
|
| 265 |
+
Args:
|
| 266 |
+
request (google.auth.transport.Request): The object used to make
|
| 267 |
+
HTTP requests.
|
| 268 |
+
|
| 269 |
+
Raises:
|
| 270 |
+
google.auth.exceptions.RefreshError: If the credentials could
|
| 271 |
+
not be refreshed.
|
| 272 |
+
"""
|
| 273 |
+
if not self.can_refresh:
|
| 274 |
+
raise exceptions.RefreshError(
|
| 275 |
+
"The credentials do not contain the necessary fields need to "
|
| 276 |
+
"refresh the access token. You must specify refresh_token, "
|
| 277 |
+
"token_url, client_id, and client_secret."
|
| 278 |
+
)
|
| 279 |
+
|
| 280 |
+
now = _helpers.utcnow()
|
| 281 |
+
response_data = self._make_sts_request(request)
|
| 282 |
+
|
| 283 |
+
self.token = response_data.get("access_token")
|
| 284 |
+
|
| 285 |
+
lifetime = datetime.timedelta(seconds=response_data.get("expires_in"))
|
| 286 |
+
self.expiry = now + lifetime
|
| 287 |
+
|
| 288 |
+
if "refresh_token" in response_data:
|
| 289 |
+
self._refresh_token = response_data["refresh_token"]
|
| 290 |
+
|
| 291 |
+
def _make_sts_request(self, request):
|
| 292 |
+
return self._sts_client.refresh_token(request, self._refresh_token)
|
| 293 |
+
|
| 294 |
+
@_helpers.copy_docstring(credentials.Credentials)
|
| 295 |
+
def get_cred_info(self):
|
| 296 |
+
if self._cred_file_path:
|
| 297 |
+
return {
|
| 298 |
+
"credential_source": self._cred_file_path,
|
| 299 |
+
"credential_type": "external account authorized user credentials",
|
| 300 |
+
}
|
| 301 |
+
return None
|
| 302 |
+
|
| 303 |
+
def _make_copy(self):
|
| 304 |
+
kwargs = self.constructor_args()
|
| 305 |
+
cred = self.__class__(**kwargs)
|
| 306 |
+
cred._cred_file_path = self._cred_file_path
|
| 307 |
+
return cred
|
| 308 |
+
|
| 309 |
+
@_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
|
| 310 |
+
def with_quota_project(self, quota_project_id):
|
| 311 |
+
cred = self._make_copy()
|
| 312 |
+
cred._quota_project_id = quota_project_id
|
| 313 |
+
return cred
|
| 314 |
+
|
| 315 |
+
@_helpers.copy_docstring(credentials.CredentialsWithTokenUri)
|
| 316 |
+
def with_token_uri(self, token_uri):
|
| 317 |
+
cred = self._make_copy()
|
| 318 |
+
cred._token_url = token_uri
|
| 319 |
+
return cred
|
| 320 |
+
|
| 321 |
+
@_helpers.copy_docstring(credentials.CredentialsWithUniverseDomain)
|
| 322 |
+
def with_universe_domain(self, universe_domain):
|
| 323 |
+
cred = self._make_copy()
|
| 324 |
+
cred._universe_domain = universe_domain
|
| 325 |
+
return cred
|
| 326 |
+
|
| 327 |
+
@classmethod
|
| 328 |
+
def from_info(cls, info, **kwargs):
|
| 329 |
+
"""Creates a Credentials instance from parsed external account info.
|
| 330 |
+
|
| 331 |
+
Args:
|
| 332 |
+
info (Mapping[str, str]): The external account info in Google
|
| 333 |
+
format.
|
| 334 |
+
kwargs: Additional arguments to pass to the constructor.
|
| 335 |
+
|
| 336 |
+
Returns:
|
| 337 |
+
google.auth.external_account_authorized_user.Credentials: The
|
| 338 |
+
constructed credentials.
|
| 339 |
+
|
| 340 |
+
Raises:
|
| 341 |
+
ValueError: For invalid parameters.
|
| 342 |
+
"""
|
| 343 |
+
expiry = info.get("expiry")
|
| 344 |
+
if expiry:
|
| 345 |
+
expiry = datetime.datetime.strptime(
|
| 346 |
+
expiry.rstrip("Z").split(".")[0], "%Y-%m-%dT%H:%M:%S"
|
| 347 |
+
)
|
| 348 |
+
return cls(
|
| 349 |
+
audience=info.get("audience"),
|
| 350 |
+
refresh_token=info.get("refresh_token"),
|
| 351 |
+
token_url=info.get("token_url"),
|
| 352 |
+
token_info_url=info.get("token_info_url"),
|
| 353 |
+
client_id=info.get("client_id"),
|
| 354 |
+
client_secret=info.get("client_secret"),
|
| 355 |
+
token=info.get("token"),
|
| 356 |
+
expiry=expiry,
|
| 357 |
+
revoke_url=info.get("revoke_url"),
|
| 358 |
+
quota_project_id=info.get("quota_project_id"),
|
| 359 |
+
scopes=info.get("scopes"),
|
| 360 |
+
universe_domain=info.get(
|
| 361 |
+
"universe_domain", credentials.DEFAULT_UNIVERSE_DOMAIN
|
| 362 |
+
),
|
| 363 |
+
**kwargs
|
| 364 |
+
)
|
| 365 |
+
|
| 366 |
+
@classmethod
|
| 367 |
+
def from_file(cls, filename, **kwargs):
|
| 368 |
+
"""Creates a Credentials instance from an external account json file.
|
| 369 |
+
|
| 370 |
+
Args:
|
| 371 |
+
filename (str): The path to the external account json file.
|
| 372 |
+
kwargs: Additional arguments to pass to the constructor.
|
| 373 |
+
|
| 374 |
+
Returns:
|
| 375 |
+
google.auth.external_account_authorized_user.Credentials: The
|
| 376 |
+
constructed credentials.
|
| 377 |
+
"""
|
| 378 |
+
with io.open(filename, "r", encoding="utf-8") as json_file:
|
| 379 |
+
data = json.load(json_file)
|
| 380 |
+
return cls.from_info(data, **kwargs)
|
.venv/lib/python3.11/site-packages/google/auth/transport/_custom_tls_signer.py
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2022 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""
|
| 16 |
+
Code for configuring client side TLS to offload the signing operation to
|
| 17 |
+
signing libraries.
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
import ctypes
|
| 21 |
+
import json
|
| 22 |
+
import logging
|
| 23 |
+
import os
|
| 24 |
+
import sys
|
| 25 |
+
|
| 26 |
+
import cffi # type: ignore
|
| 27 |
+
|
| 28 |
+
from google.auth import exceptions
|
| 29 |
+
|
| 30 |
+
_LOGGER = logging.getLogger(__name__)
|
| 31 |
+
|
| 32 |
+
# C++ offload lib requires google-auth lib to provide the following callback:
|
| 33 |
+
# using SignFunc = int (*)(unsigned char *sig, size_t *sig_len,
|
| 34 |
+
# const unsigned char *tbs, size_t tbs_len)
|
| 35 |
+
# The bytes to be signed and the length are provided via `tbs` and `tbs_len`,
|
| 36 |
+
# the callback computes the signature, and write the signature and its length
|
| 37 |
+
# into `sig` and `sig_len`.
|
| 38 |
+
# If the signing is successful, the callback returns 1, otherwise it returns 0.
|
| 39 |
+
SIGN_CALLBACK_CTYPE = ctypes.CFUNCTYPE(
|
| 40 |
+
ctypes.c_int, # return type
|
| 41 |
+
ctypes.POINTER(ctypes.c_ubyte), # sig
|
| 42 |
+
ctypes.POINTER(ctypes.c_size_t), # sig_len
|
| 43 |
+
ctypes.POINTER(ctypes.c_ubyte), # tbs
|
| 44 |
+
ctypes.c_size_t, # tbs_len
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
# Cast SSL_CTX* to void*
|
| 49 |
+
def _cast_ssl_ctx_to_void_p_pyopenssl(ssl_ctx):
|
| 50 |
+
return ctypes.cast(int(cffi.FFI().cast("intptr_t", ssl_ctx)), ctypes.c_void_p)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
# Cast SSL_CTX* to void*
|
| 54 |
+
def _cast_ssl_ctx_to_void_p_stdlib(context):
|
| 55 |
+
return ctypes.c_void_p.from_address(
|
| 56 |
+
id(context) + ctypes.sizeof(ctypes.c_void_p) * 2
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
# Load offload library and set up the function types.
|
| 61 |
+
def load_offload_lib(offload_lib_path):
|
| 62 |
+
_LOGGER.debug("loading offload library from %s", offload_lib_path)
|
| 63 |
+
|
| 64 |
+
# winmode parameter is only available for python 3.8+.
|
| 65 |
+
lib = (
|
| 66 |
+
ctypes.CDLL(offload_lib_path, winmode=0)
|
| 67 |
+
if sys.version_info >= (3, 8) and os.name == "nt"
|
| 68 |
+
else ctypes.CDLL(offload_lib_path)
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
# Set up types for:
|
| 72 |
+
# int ConfigureSslContext(SignFunc sign_func, const char *cert, SSL_CTX *ctx)
|
| 73 |
+
lib.ConfigureSslContext.argtypes = [
|
| 74 |
+
SIGN_CALLBACK_CTYPE,
|
| 75 |
+
ctypes.c_char_p,
|
| 76 |
+
ctypes.c_void_p,
|
| 77 |
+
]
|
| 78 |
+
lib.ConfigureSslContext.restype = ctypes.c_int
|
| 79 |
+
|
| 80 |
+
return lib
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
# Load signer library and set up the function types.
|
| 84 |
+
# See: https://github.com/googleapis/enterprise-certificate-proxy/blob/main/cshared/main.go
|
| 85 |
+
def load_signer_lib(signer_lib_path):
|
| 86 |
+
_LOGGER.debug("loading signer library from %s", signer_lib_path)
|
| 87 |
+
|
| 88 |
+
# winmode parameter is only available for python 3.8+.
|
| 89 |
+
lib = (
|
| 90 |
+
ctypes.CDLL(signer_lib_path, winmode=0)
|
| 91 |
+
if sys.version_info >= (3, 8) and os.name == "nt"
|
| 92 |
+
else ctypes.CDLL(signer_lib_path)
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
# Set up types for:
|
| 96 |
+
# func GetCertPemForPython(configFilePath *C.char, certHolder *byte, certHolderLen int)
|
| 97 |
+
lib.GetCertPemForPython.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_int]
|
| 98 |
+
# Returns: certLen
|
| 99 |
+
lib.GetCertPemForPython.restype = ctypes.c_int
|
| 100 |
+
|
| 101 |
+
# Set up types for:
|
| 102 |
+
# func SignForPython(configFilePath *C.char, digest *byte, digestLen int,
|
| 103 |
+
# sigHolder *byte, sigHolderLen int)
|
| 104 |
+
lib.SignForPython.argtypes = [
|
| 105 |
+
ctypes.c_char_p,
|
| 106 |
+
ctypes.c_char_p,
|
| 107 |
+
ctypes.c_int,
|
| 108 |
+
ctypes.c_char_p,
|
| 109 |
+
ctypes.c_int,
|
| 110 |
+
]
|
| 111 |
+
# Returns: the signature length
|
| 112 |
+
lib.SignForPython.restype = ctypes.c_int
|
| 113 |
+
|
| 114 |
+
return lib
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def load_provider_lib(provider_lib_path):
|
| 118 |
+
_LOGGER.debug("loading provider library from %s", provider_lib_path)
|
| 119 |
+
|
| 120 |
+
# winmode parameter is only available for python 3.8+.
|
| 121 |
+
lib = (
|
| 122 |
+
ctypes.CDLL(provider_lib_path, winmode=0)
|
| 123 |
+
if sys.version_info >= (3, 8) and os.name == "nt"
|
| 124 |
+
else ctypes.CDLL(provider_lib_path)
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
lib.ECP_attach_to_ctx.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
|
| 128 |
+
lib.ECP_attach_to_ctx.restype = ctypes.c_int
|
| 129 |
+
|
| 130 |
+
return lib
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
# Computes SHA256 hash.
|
| 134 |
+
def _compute_sha256_digest(to_be_signed, to_be_signed_len):
|
| 135 |
+
from cryptography.hazmat.primitives import hashes
|
| 136 |
+
|
| 137 |
+
data = ctypes.string_at(to_be_signed, to_be_signed_len)
|
| 138 |
+
hash = hashes.Hash(hashes.SHA256())
|
| 139 |
+
hash.update(data)
|
| 140 |
+
return hash.finalize()
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
# Create the signing callback. The actual signing work is done by the
|
| 144 |
+
# `SignForPython` method from the signer lib.
|
| 145 |
+
def get_sign_callback(signer_lib, config_file_path):
|
| 146 |
+
def sign_callback(sig, sig_len, tbs, tbs_len):
|
| 147 |
+
_LOGGER.debug("calling sign callback...")
|
| 148 |
+
|
| 149 |
+
digest = _compute_sha256_digest(tbs, tbs_len)
|
| 150 |
+
digestArray = ctypes.c_char * len(digest)
|
| 151 |
+
|
| 152 |
+
# reserve 2000 bytes for the signature, shoud be more then enough.
|
| 153 |
+
# RSA signature is 256 bytes, EC signature is 70~72.
|
| 154 |
+
sig_holder_len = 2000
|
| 155 |
+
sig_holder = ctypes.create_string_buffer(sig_holder_len)
|
| 156 |
+
|
| 157 |
+
signature_len = signer_lib.SignForPython(
|
| 158 |
+
config_file_path.encode(), # configFilePath
|
| 159 |
+
digestArray.from_buffer(bytearray(digest)), # digest
|
| 160 |
+
len(digest), # digestLen
|
| 161 |
+
sig_holder, # sigHolder
|
| 162 |
+
sig_holder_len, # sigHolderLen
|
| 163 |
+
)
|
| 164 |
+
|
| 165 |
+
if signature_len == 0:
|
| 166 |
+
# signing failed, return 0
|
| 167 |
+
return 0
|
| 168 |
+
|
| 169 |
+
sig_len[0] = signature_len
|
| 170 |
+
bs = bytearray(sig_holder)
|
| 171 |
+
for i in range(signature_len):
|
| 172 |
+
sig[i] = bs[i]
|
| 173 |
+
|
| 174 |
+
return 1
|
| 175 |
+
|
| 176 |
+
return SIGN_CALLBACK_CTYPE(sign_callback)
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
# Obtain the certificate bytes by calling the `GetCertPemForPython` method from
|
| 180 |
+
# the signer lib. The method is called twice, the first time is to compute the
|
| 181 |
+
# cert length, then we create a buffer to hold the cert, and call it again to
|
| 182 |
+
# fill the buffer.
|
| 183 |
+
def get_cert(signer_lib, config_file_path):
|
| 184 |
+
# First call to calculate the cert length
|
| 185 |
+
cert_len = signer_lib.GetCertPemForPython(
|
| 186 |
+
config_file_path.encode(), # configFilePath
|
| 187 |
+
None, # certHolder
|
| 188 |
+
0, # certHolderLen
|
| 189 |
+
)
|
| 190 |
+
if cert_len == 0:
|
| 191 |
+
raise exceptions.MutualTLSChannelError("failed to get certificate")
|
| 192 |
+
|
| 193 |
+
# Then we create an array to hold the cert, and call again to fill the cert
|
| 194 |
+
cert_holder = ctypes.create_string_buffer(cert_len)
|
| 195 |
+
signer_lib.GetCertPemForPython(
|
| 196 |
+
config_file_path.encode(), # configFilePath
|
| 197 |
+
cert_holder, # certHolder
|
| 198 |
+
cert_len, # certHolderLen
|
| 199 |
+
)
|
| 200 |
+
return bytes(cert_holder)
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
class CustomTlsSigner(object):
|
| 204 |
+
def __init__(self, enterprise_cert_file_path):
|
| 205 |
+
"""
|
| 206 |
+
This class loads the offload and signer library, and calls APIs from
|
| 207 |
+
these libraries to obtain the cert and a signing callback, and attach
|
| 208 |
+
them to SSL context. The cert and the signing callback will be used
|
| 209 |
+
for client authentication in TLS handshake.
|
| 210 |
+
|
| 211 |
+
Args:
|
| 212 |
+
enterprise_cert_file_path (str): the path to a enterprise cert JSON
|
| 213 |
+
file. The file should contain the following field:
|
| 214 |
+
|
| 215 |
+
{
|
| 216 |
+
"libs": {
|
| 217 |
+
"ecp_client": "...",
|
| 218 |
+
"tls_offload": "..."
|
| 219 |
+
}
|
| 220 |
+
}
|
| 221 |
+
"""
|
| 222 |
+
self._enterprise_cert_file_path = enterprise_cert_file_path
|
| 223 |
+
self._cert = None
|
| 224 |
+
self._sign_callback = None
|
| 225 |
+
self._provider_lib = None
|
| 226 |
+
|
| 227 |
+
def load_libraries(self):
|
| 228 |
+
with open(self._enterprise_cert_file_path, "r") as f:
|
| 229 |
+
enterprise_cert_json = json.load(f)
|
| 230 |
+
libs = enterprise_cert_json.get("libs", {})
|
| 231 |
+
|
| 232 |
+
signer_library = libs.get("ecp_client", None)
|
| 233 |
+
offload_library = libs.get("tls_offload", None)
|
| 234 |
+
provider_library = libs.get("ecp_provider", None)
|
| 235 |
+
|
| 236 |
+
# Using newer provider implementation. This is mutually exclusive to the
|
| 237 |
+
# offload implementation.
|
| 238 |
+
if provider_library:
|
| 239 |
+
self._provider_lib = load_provider_lib(provider_library)
|
| 240 |
+
return
|
| 241 |
+
|
| 242 |
+
# Using old offload implementation
|
| 243 |
+
if offload_library and signer_library:
|
| 244 |
+
self._offload_lib = load_offload_lib(offload_library)
|
| 245 |
+
self._signer_lib = load_signer_lib(signer_library)
|
| 246 |
+
self.set_up_custom_key()
|
| 247 |
+
return
|
| 248 |
+
|
| 249 |
+
raise exceptions.MutualTLSChannelError("enterprise cert file is invalid")
|
| 250 |
+
|
| 251 |
+
def set_up_custom_key(self):
|
| 252 |
+
# We need to keep a reference of the cert and sign callback so it won't
|
| 253 |
+
# be garbage collected, otherwise it will crash when used by signer lib.
|
| 254 |
+
self._cert = get_cert(self._signer_lib, self._enterprise_cert_file_path)
|
| 255 |
+
self._sign_callback = get_sign_callback(
|
| 256 |
+
self._signer_lib, self._enterprise_cert_file_path
|
| 257 |
+
)
|
| 258 |
+
|
| 259 |
+
def should_use_provider(self):
|
| 260 |
+
if self._provider_lib:
|
| 261 |
+
return True
|
| 262 |
+
return False
|
| 263 |
+
|
| 264 |
+
def attach_to_ssl_context(self, ctx):
|
| 265 |
+
if self.should_use_provider():
|
| 266 |
+
if not self._provider_lib.ECP_attach_to_ctx(
|
| 267 |
+
_cast_ssl_ctx_to_void_p_stdlib(ctx),
|
| 268 |
+
self._enterprise_cert_file_path.encode("ascii"),
|
| 269 |
+
):
|
| 270 |
+
raise exceptions.MutualTLSChannelError(
|
| 271 |
+
"failed to configure ECP Provider SSL context"
|
| 272 |
+
)
|
| 273 |
+
elif self._offload_lib and self._signer_lib:
|
| 274 |
+
if not self._offload_lib.ConfigureSslContext(
|
| 275 |
+
self._sign_callback,
|
| 276 |
+
ctypes.c_char_p(self._cert),
|
| 277 |
+
_cast_ssl_ctx_to_void_p_pyopenssl(ctx._ctx._context),
|
| 278 |
+
):
|
| 279 |
+
raise exceptions.MutualTLSChannelError(
|
| 280 |
+
"failed to configure ECP Offload SSL context"
|
| 281 |
+
)
|
| 282 |
+
else:
|
| 283 |
+
raise exceptions.MutualTLSChannelError("Invalid ECP configuration.")
|
.venv/lib/python3.11/site-packages/google/auth/transport/_requests_base.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""Transport adapter for Base Requests."""
|
| 16 |
+
# NOTE: The coverage for this file is temporarily disabled in `.coveragerc`
|
| 17 |
+
# since it is currently unused.
|
| 18 |
+
|
| 19 |
+
import abc
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
_DEFAULT_TIMEOUT = 120 # in second
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class _BaseAuthorizedSession(metaclass=abc.ABCMeta):
|
| 26 |
+
"""Base class for a Request Session with credentials. This class is intended to capture
|
| 27 |
+
the common logic between synchronous and asynchronous request sessions and is not intended to
|
| 28 |
+
be instantiated directly.
|
| 29 |
+
|
| 30 |
+
Args:
|
| 31 |
+
credentials (google.auth._credentials_base.BaseCredentials): The credentials to
|
| 32 |
+
add to the request.
|
| 33 |
+
"""
|
| 34 |
+
|
| 35 |
+
def __init__(self, credentials):
|
| 36 |
+
self.credentials = credentials
|
| 37 |
+
|
| 38 |
+
@abc.abstractmethod
|
| 39 |
+
def request(
|
| 40 |
+
self,
|
| 41 |
+
method,
|
| 42 |
+
url,
|
| 43 |
+
data=None,
|
| 44 |
+
headers=None,
|
| 45 |
+
max_allowed_time=None,
|
| 46 |
+
timeout=_DEFAULT_TIMEOUT,
|
| 47 |
+
**kwargs
|
| 48 |
+
):
|
| 49 |
+
raise NotImplementedError("Request must be implemented")
|
| 50 |
+
|
| 51 |
+
@abc.abstractmethod
|
| 52 |
+
def close(self):
|
| 53 |
+
raise NotImplementedError("Close must be implemented")
|
.venv/lib/python3.11/site-packages/google/auth/transport/grpc.py
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2016 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""Authorization support for gRPC."""
|
| 16 |
+
|
| 17 |
+
from __future__ import absolute_import
|
| 18 |
+
|
| 19 |
+
import logging
|
| 20 |
+
import os
|
| 21 |
+
|
| 22 |
+
from google.auth import environment_vars
|
| 23 |
+
from google.auth import exceptions
|
| 24 |
+
from google.auth.transport import _mtls_helper
|
| 25 |
+
from google.oauth2 import service_account
|
| 26 |
+
|
| 27 |
+
try:
|
| 28 |
+
import grpc # type: ignore
|
| 29 |
+
except ImportError as caught_exc: # pragma: NO COVER
|
| 30 |
+
raise ImportError(
|
| 31 |
+
"gRPC is not installed from please install the grpcio package to use the gRPC transport."
|
| 32 |
+
) from caught_exc
|
| 33 |
+
|
| 34 |
+
_LOGGER = logging.getLogger(__name__)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class AuthMetadataPlugin(grpc.AuthMetadataPlugin):
|
| 38 |
+
"""A `gRPC AuthMetadataPlugin`_ that inserts the credentials into each
|
| 39 |
+
request.
|
| 40 |
+
|
| 41 |
+
.. _gRPC AuthMetadataPlugin:
|
| 42 |
+
http://www.grpc.io/grpc/python/grpc.html#grpc.AuthMetadataPlugin
|
| 43 |
+
|
| 44 |
+
Args:
|
| 45 |
+
credentials (google.auth.credentials.Credentials): The credentials to
|
| 46 |
+
add to requests.
|
| 47 |
+
request (google.auth.transport.Request): A HTTP transport request
|
| 48 |
+
object used to refresh credentials as needed.
|
| 49 |
+
default_host (Optional[str]): A host like "pubsub.googleapis.com".
|
| 50 |
+
This is used when a self-signed JWT is created from service
|
| 51 |
+
account credentials.
|
| 52 |
+
"""
|
| 53 |
+
|
| 54 |
+
def __init__(self, credentials, request, default_host=None):
|
| 55 |
+
# pylint: disable=no-value-for-parameter
|
| 56 |
+
# pylint doesn't realize that the super method takes no arguments
|
| 57 |
+
# because this class is the same name as the superclass.
|
| 58 |
+
super(AuthMetadataPlugin, self).__init__()
|
| 59 |
+
self._credentials = credentials
|
| 60 |
+
self._request = request
|
| 61 |
+
self._default_host = default_host
|
| 62 |
+
|
| 63 |
+
def _get_authorization_headers(self, context):
|
| 64 |
+
"""Gets the authorization headers for a request.
|
| 65 |
+
|
| 66 |
+
Returns:
|
| 67 |
+
Sequence[Tuple[str, str]]: A list of request headers (key, value)
|
| 68 |
+
to add to the request.
|
| 69 |
+
"""
|
| 70 |
+
headers = {}
|
| 71 |
+
|
| 72 |
+
# https://google.aip.dev/auth/4111
|
| 73 |
+
# Attempt to use self-signed JWTs when a service account is used.
|
| 74 |
+
# A default host must be explicitly provided since it cannot always
|
| 75 |
+
# be determined from the context.service_url.
|
| 76 |
+
if isinstance(self._credentials, service_account.Credentials):
|
| 77 |
+
self._credentials._create_self_signed_jwt(
|
| 78 |
+
"https://{}/".format(self._default_host) if self._default_host else None
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
self._credentials.before_request(
|
| 82 |
+
self._request, context.method_name, context.service_url, headers
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
return list(headers.items())
|
| 86 |
+
|
| 87 |
+
def __call__(self, context, callback):
|
| 88 |
+
"""Passes authorization metadata into the given callback.
|
| 89 |
+
|
| 90 |
+
Args:
|
| 91 |
+
context (grpc.AuthMetadataContext): The RPC context.
|
| 92 |
+
callback (grpc.AuthMetadataPluginCallback): The callback that will
|
| 93 |
+
be invoked to pass in the authorization metadata.
|
| 94 |
+
"""
|
| 95 |
+
callback(self._get_authorization_headers(context), None)
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def secure_authorized_channel(
|
| 99 |
+
credentials,
|
| 100 |
+
request,
|
| 101 |
+
target,
|
| 102 |
+
ssl_credentials=None,
|
| 103 |
+
client_cert_callback=None,
|
| 104 |
+
**kwargs
|
| 105 |
+
):
|
| 106 |
+
"""Creates a secure authorized gRPC channel.
|
| 107 |
+
|
| 108 |
+
This creates a channel with SSL and :class:`AuthMetadataPlugin`. This
|
| 109 |
+
channel can be used to create a stub that can make authorized requests.
|
| 110 |
+
Users can configure client certificate or rely on device certificates to
|
| 111 |
+
establish a mutual TLS channel, if the `GOOGLE_API_USE_CLIENT_CERTIFICATE`
|
| 112 |
+
variable is explicitly set to `true`.
|
| 113 |
+
|
| 114 |
+
Example::
|
| 115 |
+
|
| 116 |
+
import google.auth
|
| 117 |
+
import google.auth.transport.grpc
|
| 118 |
+
import google.auth.transport.requests
|
| 119 |
+
from google.cloud.speech.v1 import cloud_speech_pb2
|
| 120 |
+
|
| 121 |
+
# Get credentials.
|
| 122 |
+
credentials, _ = google.auth.default()
|
| 123 |
+
|
| 124 |
+
# Get an HTTP request function to refresh credentials.
|
| 125 |
+
request = google.auth.transport.requests.Request()
|
| 126 |
+
|
| 127 |
+
# Create a channel.
|
| 128 |
+
channel = google.auth.transport.grpc.secure_authorized_channel(
|
| 129 |
+
credentials, regular_endpoint, request,
|
| 130 |
+
ssl_credentials=grpc.ssl_channel_credentials())
|
| 131 |
+
|
| 132 |
+
# Use the channel to create a stub.
|
| 133 |
+
cloud_speech.create_Speech_stub(channel)
|
| 134 |
+
|
| 135 |
+
Usage:
|
| 136 |
+
|
| 137 |
+
There are actually a couple of options to create a channel, depending on if
|
| 138 |
+
you want to create a regular or mutual TLS channel.
|
| 139 |
+
|
| 140 |
+
First let's list the endpoints (regular vs mutual TLS) to choose from::
|
| 141 |
+
|
| 142 |
+
regular_endpoint = 'speech.googleapis.com:443'
|
| 143 |
+
mtls_endpoint = 'speech.mtls.googleapis.com:443'
|
| 144 |
+
|
| 145 |
+
Option 1: create a regular (non-mutual) TLS channel by explicitly setting
|
| 146 |
+
the ssl_credentials::
|
| 147 |
+
|
| 148 |
+
regular_ssl_credentials = grpc.ssl_channel_credentials()
|
| 149 |
+
|
| 150 |
+
channel = google.auth.transport.grpc.secure_authorized_channel(
|
| 151 |
+
credentials, regular_endpoint, request,
|
| 152 |
+
ssl_credentials=regular_ssl_credentials)
|
| 153 |
+
|
| 154 |
+
Option 2: create a mutual TLS channel by calling a callback which returns
|
| 155 |
+
the client side certificate and the key (Note that
|
| 156 |
+
`GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable must be explicitly
|
| 157 |
+
set to `true`)::
|
| 158 |
+
|
| 159 |
+
def my_client_cert_callback():
|
| 160 |
+
code_to_load_client_cert_and_key()
|
| 161 |
+
if loaded:
|
| 162 |
+
return (pem_cert_bytes, pem_key_bytes)
|
| 163 |
+
raise MyClientCertFailureException()
|
| 164 |
+
|
| 165 |
+
try:
|
| 166 |
+
channel = google.auth.transport.grpc.secure_authorized_channel(
|
| 167 |
+
credentials, mtls_endpoint, request,
|
| 168 |
+
client_cert_callback=my_client_cert_callback)
|
| 169 |
+
except MyClientCertFailureException:
|
| 170 |
+
# handle the exception
|
| 171 |
+
|
| 172 |
+
Option 3: use application default SSL credentials. It searches and uses
|
| 173 |
+
the command in a context aware metadata file, which is available on devices
|
| 174 |
+
with endpoint verification support (Note that
|
| 175 |
+
`GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable must be explicitly
|
| 176 |
+
set to `true`).
|
| 177 |
+
See https://cloud.google.com/endpoint-verification/docs/overview::
|
| 178 |
+
|
| 179 |
+
try:
|
| 180 |
+
default_ssl_credentials = SslCredentials()
|
| 181 |
+
except:
|
| 182 |
+
# Exception can be raised if the context aware metadata is malformed.
|
| 183 |
+
# See :class:`SslCredentials` for the possible exceptions.
|
| 184 |
+
|
| 185 |
+
# Choose the endpoint based on the SSL credentials type.
|
| 186 |
+
if default_ssl_credentials.is_mtls:
|
| 187 |
+
endpoint_to_use = mtls_endpoint
|
| 188 |
+
else:
|
| 189 |
+
endpoint_to_use = regular_endpoint
|
| 190 |
+
channel = google.auth.transport.grpc.secure_authorized_channel(
|
| 191 |
+
credentials, endpoint_to_use, request,
|
| 192 |
+
ssl_credentials=default_ssl_credentials)
|
| 193 |
+
|
| 194 |
+
Option 4: not setting ssl_credentials and client_cert_callback. For devices
|
| 195 |
+
without endpoint verification support or `GOOGLE_API_USE_CLIENT_CERTIFICATE`
|
| 196 |
+
environment variable is not `true`, a regular TLS channel is created;
|
| 197 |
+
otherwise, a mutual TLS channel is created, however, the call should be
|
| 198 |
+
wrapped in a try/except block in case of malformed context aware metadata.
|
| 199 |
+
|
| 200 |
+
The following code uses regular_endpoint, it works the same no matter the
|
| 201 |
+
created channle is regular or mutual TLS. Regular endpoint ignores client
|
| 202 |
+
certificate and key::
|
| 203 |
+
|
| 204 |
+
channel = google.auth.transport.grpc.secure_authorized_channel(
|
| 205 |
+
credentials, regular_endpoint, request)
|
| 206 |
+
|
| 207 |
+
The following code uses mtls_endpoint, if the created channle is regular,
|
| 208 |
+
and API mtls_endpoint is confgured to require client SSL credentials, API
|
| 209 |
+
calls using this channel will be rejected::
|
| 210 |
+
|
| 211 |
+
channel = google.auth.transport.grpc.secure_authorized_channel(
|
| 212 |
+
credentials, mtls_endpoint, request)
|
| 213 |
+
|
| 214 |
+
Args:
|
| 215 |
+
credentials (google.auth.credentials.Credentials): The credentials to
|
| 216 |
+
add to requests.
|
| 217 |
+
request (google.auth.transport.Request): A HTTP transport request
|
| 218 |
+
object used to refresh credentials as needed. Even though gRPC
|
| 219 |
+
is a separate transport, there's no way to refresh the credentials
|
| 220 |
+
without using a standard http transport.
|
| 221 |
+
target (str): The host and port of the service.
|
| 222 |
+
ssl_credentials (grpc.ChannelCredentials): Optional SSL channel
|
| 223 |
+
credentials. This can be used to specify different certificates.
|
| 224 |
+
This argument is mutually exclusive with client_cert_callback;
|
| 225 |
+
providing both will raise an exception.
|
| 226 |
+
If ssl_credentials and client_cert_callback are None, application
|
| 227 |
+
default SSL credentials are used if `GOOGLE_API_USE_CLIENT_CERTIFICATE`
|
| 228 |
+
environment variable is explicitly set to `true`, otherwise one way TLS
|
| 229 |
+
SSL credentials are used.
|
| 230 |
+
client_cert_callback (Callable[[], (bytes, bytes)]): Optional
|
| 231 |
+
callback function to obtain client certicate and key for mutual TLS
|
| 232 |
+
connection. This argument is mutually exclusive with
|
| 233 |
+
ssl_credentials; providing both will raise an exception.
|
| 234 |
+
This argument does nothing unless `GOOGLE_API_USE_CLIENT_CERTIFICATE`
|
| 235 |
+
environment variable is explicitly set to `true`.
|
| 236 |
+
kwargs: Additional arguments to pass to :func:`grpc.secure_channel`.
|
| 237 |
+
|
| 238 |
+
Returns:
|
| 239 |
+
grpc.Channel: The created gRPC channel.
|
| 240 |
+
|
| 241 |
+
Raises:
|
| 242 |
+
google.auth.exceptions.MutualTLSChannelError: If mutual TLS channel
|
| 243 |
+
creation failed for any reason.
|
| 244 |
+
"""
|
| 245 |
+
# Create the metadata plugin for inserting the authorization header.
|
| 246 |
+
metadata_plugin = AuthMetadataPlugin(credentials, request)
|
| 247 |
+
|
| 248 |
+
# Create a set of grpc.CallCredentials using the metadata plugin.
|
| 249 |
+
google_auth_credentials = grpc.metadata_call_credentials(metadata_plugin)
|
| 250 |
+
|
| 251 |
+
if ssl_credentials and client_cert_callback:
|
| 252 |
+
raise exceptions.MalformedError(
|
| 253 |
+
"Received both ssl_credentials and client_cert_callback; "
|
| 254 |
+
"these are mutually exclusive."
|
| 255 |
+
)
|
| 256 |
+
|
| 257 |
+
# If SSL credentials are not explicitly set, try client_cert_callback and ADC.
|
| 258 |
+
if not ssl_credentials:
|
| 259 |
+
use_client_cert = os.getenv(
|
| 260 |
+
environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE, "false"
|
| 261 |
+
)
|
| 262 |
+
if use_client_cert == "true" and client_cert_callback:
|
| 263 |
+
# Use the callback if provided.
|
| 264 |
+
cert, key = client_cert_callback()
|
| 265 |
+
ssl_credentials = grpc.ssl_channel_credentials(
|
| 266 |
+
certificate_chain=cert, private_key=key
|
| 267 |
+
)
|
| 268 |
+
elif use_client_cert == "true":
|
| 269 |
+
# Use application default SSL credentials.
|
| 270 |
+
adc_ssl_credentils = SslCredentials()
|
| 271 |
+
ssl_credentials = adc_ssl_credentils.ssl_credentials
|
| 272 |
+
else:
|
| 273 |
+
ssl_credentials = grpc.ssl_channel_credentials()
|
| 274 |
+
|
| 275 |
+
# Combine the ssl credentials and the authorization credentials.
|
| 276 |
+
composite_credentials = grpc.composite_channel_credentials(
|
| 277 |
+
ssl_credentials, google_auth_credentials
|
| 278 |
+
)
|
| 279 |
+
|
| 280 |
+
return grpc.secure_channel(target, composite_credentials, **kwargs)
|
| 281 |
+
|
| 282 |
+
|
| 283 |
+
class SslCredentials:
|
| 284 |
+
"""Class for application default SSL credentials.
|
| 285 |
+
|
| 286 |
+
The behavior is controlled by `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment
|
| 287 |
+
variable whose default value is `false`. Client certificate will not be used
|
| 288 |
+
unless the environment variable is explicitly set to `true`. See
|
| 289 |
+
https://google.aip.dev/auth/4114
|
| 290 |
+
|
| 291 |
+
If the environment variable is `true`, then for devices with endpoint verification
|
| 292 |
+
support, a device certificate will be automatically loaded and mutual TLS will
|
| 293 |
+
be established.
|
| 294 |
+
See https://cloud.google.com/endpoint-verification/docs/overview.
|
| 295 |
+
"""
|
| 296 |
+
|
| 297 |
+
def __init__(self):
|
| 298 |
+
use_client_cert = os.getenv(
|
| 299 |
+
environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE, "false"
|
| 300 |
+
)
|
| 301 |
+
if use_client_cert != "true":
|
| 302 |
+
self._is_mtls = False
|
| 303 |
+
else:
|
| 304 |
+
# Load client SSL credentials.
|
| 305 |
+
metadata_path = _mtls_helper._check_config_path(
|
| 306 |
+
_mtls_helper.CONTEXT_AWARE_METADATA_PATH
|
| 307 |
+
)
|
| 308 |
+
self._is_mtls = metadata_path is not None
|
| 309 |
+
|
| 310 |
+
@property
|
| 311 |
+
def ssl_credentials(self):
|
| 312 |
+
"""Get the created SSL channel credentials.
|
| 313 |
+
|
| 314 |
+
For devices with endpoint verification support, if the device certificate
|
| 315 |
+
loading has any problems, corresponding exceptions will be raised. For
|
| 316 |
+
a device without endpoint verification support, no exceptions will be
|
| 317 |
+
raised.
|
| 318 |
+
|
| 319 |
+
Returns:
|
| 320 |
+
grpc.ChannelCredentials: The created grpc channel credentials.
|
| 321 |
+
|
| 322 |
+
Raises:
|
| 323 |
+
google.auth.exceptions.MutualTLSChannelError: If mutual TLS channel
|
| 324 |
+
creation failed for any reason.
|
| 325 |
+
"""
|
| 326 |
+
if self._is_mtls:
|
| 327 |
+
try:
|
| 328 |
+
_, cert, key, _ = _mtls_helper.get_client_ssl_credentials()
|
| 329 |
+
self._ssl_credentials = grpc.ssl_channel_credentials(
|
| 330 |
+
certificate_chain=cert, private_key=key
|
| 331 |
+
)
|
| 332 |
+
except exceptions.ClientCertError as caught_exc:
|
| 333 |
+
new_exc = exceptions.MutualTLSChannelError(caught_exc)
|
| 334 |
+
raise new_exc from caught_exc
|
| 335 |
+
else:
|
| 336 |
+
self._ssl_credentials = grpc.ssl_channel_credentials()
|
| 337 |
+
|
| 338 |
+
return self._ssl_credentials
|
| 339 |
+
|
| 340 |
+
@property
|
| 341 |
+
def is_mtls(self):
|
| 342 |
+
"""Indicates if the created SSL channel credentials is mutual TLS."""
|
| 343 |
+
return self._is_mtls
|
.venv/lib/python3.11/site-packages/google/oauth2/__init__.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2016 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""Google OAuth 2.0 Library for Python."""
|
| 16 |
+
|
| 17 |
+
import sys
|
| 18 |
+
import warnings
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class Python37DeprecationWarning(DeprecationWarning): # pragma: NO COVER
|
| 22 |
+
"""
|
| 23 |
+
Deprecation warning raised when Python 3.7 runtime is detected.
|
| 24 |
+
Python 3.7 support will be dropped after January 1, 2024.
|
| 25 |
+
"""
|
| 26 |
+
|
| 27 |
+
pass
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
# Checks if the current runtime is Python 3.7.
|
| 31 |
+
if sys.version_info.major == 3 and sys.version_info.minor == 7: # pragma: NO COVER
|
| 32 |
+
message = (
|
| 33 |
+
"After January 1, 2024, new releases of this library will drop support "
|
| 34 |
+
"for Python 3.7."
|
| 35 |
+
)
|
| 36 |
+
warnings.warn(message, Python37DeprecationWarning)
|
.venv/lib/python3.11/site-packages/google/oauth2/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (1.05 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/google/oauth2/__pycache__/_client.cpython-311.pyc
ADDED
|
Binary file (18.1 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/google/oauth2/__pycache__/_client_async.cpython-311.pyc
ADDED
|
Binary file (10.7 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/google/oauth2/__pycache__/_credentials_async.cpython-311.pyc
ADDED
|
Binary file (4.93 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/google/oauth2/__pycache__/_id_token_async.cpython-311.pyc
ADDED
|
Binary file (11 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/google/oauth2/__pycache__/_reauth_async.cpython-311.pyc
ADDED
|
Binary file (12.3 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/google/oauth2/__pycache__/_service_account_async.cpython-311.pyc
ADDED
|
Binary file (5.62 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/google/oauth2/__pycache__/challenges.cpython-311.pyc
ADDED
|
Binary file (13.4 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/google/oauth2/__pycache__/credentials.cpython-311.pyc
ADDED
|
Binary file (28.3 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/google/oauth2/__pycache__/gdch_credentials.cpython-311.pyc
ADDED
|
Binary file (9.9 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/google/oauth2/__pycache__/id_token.cpython-311.pyc
ADDED
|
Binary file (13.4 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/google/oauth2/__pycache__/reauth.cpython-311.pyc
ADDED
|
Binary file (13.1 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/google/oauth2/__pycache__/service_account.cpython-311.pyc
ADDED
|
Binary file (35.4 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/google/oauth2/__pycache__/sts.cpython-311.pyc
ADDED
|
Binary file (6.85 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/google/oauth2/__pycache__/utils.cpython-311.pyc
ADDED
|
Binary file (7.37 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/google/oauth2/__pycache__/webauthn_handler.cpython-311.pyc
ADDED
|
Binary file (4.75 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/google/oauth2/__pycache__/webauthn_handler_factory.cpython-311.pyc
ADDED
|
Binary file (1.27 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/google/oauth2/__pycache__/webauthn_types.cpython-311.pyc
ADDED
|
Binary file (8.35 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/google/oauth2/_client.py
ADDED
|
@@ -0,0 +1,508 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2016 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""OAuth 2.0 client.
|
| 16 |
+
|
| 17 |
+
This is a client for interacting with an OAuth 2.0 authorization server's
|
| 18 |
+
token endpoint.
|
| 19 |
+
|
| 20 |
+
For more information about the token endpoint, see
|
| 21 |
+
`Section 3.1 of rfc6749`_
|
| 22 |
+
|
| 23 |
+
.. _Section 3.1 of rfc6749: https://tools.ietf.org/html/rfc6749#section-3.2
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
import datetime
|
| 27 |
+
import http.client as http_client
|
| 28 |
+
import json
|
| 29 |
+
import urllib
|
| 30 |
+
|
| 31 |
+
from google.auth import _exponential_backoff
|
| 32 |
+
from google.auth import _helpers
|
| 33 |
+
from google.auth import credentials
|
| 34 |
+
from google.auth import exceptions
|
| 35 |
+
from google.auth import jwt
|
| 36 |
+
from google.auth import metrics
|
| 37 |
+
from google.auth import transport
|
| 38 |
+
|
| 39 |
+
_URLENCODED_CONTENT_TYPE = "application/x-www-form-urlencoded"
|
| 40 |
+
_JSON_CONTENT_TYPE = "application/json"
|
| 41 |
+
_JWT_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer"
|
| 42 |
+
_REFRESH_GRANT_TYPE = "refresh_token"
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def _handle_error_response(response_data, retryable_error):
|
| 46 |
+
"""Translates an error response into an exception.
|
| 47 |
+
|
| 48 |
+
Args:
|
| 49 |
+
response_data (Mapping | str): The decoded response data.
|
| 50 |
+
retryable_error Optional[bool]: A boolean indicating if an error is retryable.
|
| 51 |
+
Defaults to False.
|
| 52 |
+
|
| 53 |
+
Raises:
|
| 54 |
+
google.auth.exceptions.RefreshError: The errors contained in response_data.
|
| 55 |
+
"""
|
| 56 |
+
|
| 57 |
+
retryable_error = retryable_error if retryable_error else False
|
| 58 |
+
|
| 59 |
+
if isinstance(response_data, str):
|
| 60 |
+
raise exceptions.RefreshError(response_data, retryable=retryable_error)
|
| 61 |
+
try:
|
| 62 |
+
error_details = "{}: {}".format(
|
| 63 |
+
response_data["error"], response_data.get("error_description")
|
| 64 |
+
)
|
| 65 |
+
# If no details could be extracted, use the response data.
|
| 66 |
+
except (KeyError, ValueError):
|
| 67 |
+
error_details = json.dumps(response_data)
|
| 68 |
+
|
| 69 |
+
raise exceptions.RefreshError(
|
| 70 |
+
error_details, response_data, retryable=retryable_error
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def _can_retry(status_code, response_data):
|
| 75 |
+
"""Checks if a request can be retried by inspecting the status code
|
| 76 |
+
and response body of the request.
|
| 77 |
+
|
| 78 |
+
Args:
|
| 79 |
+
status_code (int): The response status code.
|
| 80 |
+
response_data (Mapping | str): The decoded response data.
|
| 81 |
+
|
| 82 |
+
Returns:
|
| 83 |
+
bool: True if the response is retryable. False otherwise.
|
| 84 |
+
"""
|
| 85 |
+
if status_code in transport.DEFAULT_RETRYABLE_STATUS_CODES:
|
| 86 |
+
return True
|
| 87 |
+
|
| 88 |
+
try:
|
| 89 |
+
# For a failed response, response_body could be a string
|
| 90 |
+
error_desc = response_data.get("error_description") or ""
|
| 91 |
+
error_code = response_data.get("error") or ""
|
| 92 |
+
|
| 93 |
+
if not isinstance(error_code, str) or not isinstance(error_desc, str):
|
| 94 |
+
return False
|
| 95 |
+
|
| 96 |
+
# Per Oauth 2.0 RFC https://www.rfc-editor.org/rfc/rfc6749.html#section-4.1.2.1
|
| 97 |
+
# This is needed because a redirect will not return a 500 status code.
|
| 98 |
+
retryable_error_descriptions = {
|
| 99 |
+
"internal_failure",
|
| 100 |
+
"server_error",
|
| 101 |
+
"temporarily_unavailable",
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
if any(e in retryable_error_descriptions for e in (error_code, error_desc)):
|
| 105 |
+
return True
|
| 106 |
+
|
| 107 |
+
except AttributeError:
|
| 108 |
+
pass
|
| 109 |
+
|
| 110 |
+
return False
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def _parse_expiry(response_data):
|
| 114 |
+
"""Parses the expiry field from a response into a datetime.
|
| 115 |
+
|
| 116 |
+
Args:
|
| 117 |
+
response_data (Mapping): The JSON-parsed response data.
|
| 118 |
+
|
| 119 |
+
Returns:
|
| 120 |
+
Optional[datetime]: The expiration or ``None`` if no expiration was
|
| 121 |
+
specified.
|
| 122 |
+
"""
|
| 123 |
+
expires_in = response_data.get("expires_in", None)
|
| 124 |
+
|
| 125 |
+
if expires_in is not None:
|
| 126 |
+
# Some services do not respect the OAUTH2.0 RFC and send expires_in as a
|
| 127 |
+
# JSON String.
|
| 128 |
+
if isinstance(expires_in, str):
|
| 129 |
+
expires_in = int(expires_in)
|
| 130 |
+
|
| 131 |
+
return _helpers.utcnow() + datetime.timedelta(seconds=expires_in)
|
| 132 |
+
else:
|
| 133 |
+
return None
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def _token_endpoint_request_no_throw(
|
| 137 |
+
request,
|
| 138 |
+
token_uri,
|
| 139 |
+
body,
|
| 140 |
+
access_token=None,
|
| 141 |
+
use_json=False,
|
| 142 |
+
can_retry=True,
|
| 143 |
+
headers=None,
|
| 144 |
+
**kwargs
|
| 145 |
+
):
|
| 146 |
+
"""Makes a request to the OAuth 2.0 authorization server's token endpoint.
|
| 147 |
+
This function doesn't throw on response errors.
|
| 148 |
+
|
| 149 |
+
Args:
|
| 150 |
+
request (google.auth.transport.Request): A callable used to make
|
| 151 |
+
HTTP requests.
|
| 152 |
+
token_uri (str): The OAuth 2.0 authorizations server's token endpoint
|
| 153 |
+
URI.
|
| 154 |
+
body (Mapping[str, str]): The parameters to send in the request body.
|
| 155 |
+
access_token (Optional(str)): The access token needed to make the request.
|
| 156 |
+
use_json (Optional(bool)): Use urlencoded format or json format for the
|
| 157 |
+
content type. The default value is False.
|
| 158 |
+
can_retry (bool): Enable or disable request retry behavior.
|
| 159 |
+
headers (Optional[Mapping[str, str]]): The headers for the request.
|
| 160 |
+
kwargs: Additional arguments passed on to the request method. The
|
| 161 |
+
kwargs will be passed to `requests.request` method, see:
|
| 162 |
+
https://docs.python-requests.org/en/latest/api/#requests.request.
|
| 163 |
+
For example, you can use `cert=("cert_pem_path", "key_pem_path")`
|
| 164 |
+
to set up client side SSL certificate, and use
|
| 165 |
+
`verify="ca_bundle_path"` to set up the CA certificates for sever
|
| 166 |
+
side SSL certificate verification.
|
| 167 |
+
|
| 168 |
+
Returns:
|
| 169 |
+
Tuple(bool, Mapping[str, str], Optional[bool]): A boolean indicating
|
| 170 |
+
if the request is successful, a mapping for the JSON-decoded response
|
| 171 |
+
data and in the case of an error a boolean indicating if the error
|
| 172 |
+
is retryable.
|
| 173 |
+
"""
|
| 174 |
+
if use_json:
|
| 175 |
+
headers_to_use = {"Content-Type": _JSON_CONTENT_TYPE}
|
| 176 |
+
body = json.dumps(body).encode("utf-8")
|
| 177 |
+
else:
|
| 178 |
+
headers_to_use = {"Content-Type": _URLENCODED_CONTENT_TYPE}
|
| 179 |
+
body = urllib.parse.urlencode(body).encode("utf-8")
|
| 180 |
+
|
| 181 |
+
if access_token:
|
| 182 |
+
headers_to_use["Authorization"] = "Bearer {}".format(access_token)
|
| 183 |
+
|
| 184 |
+
if headers:
|
| 185 |
+
headers_to_use.update(headers)
|
| 186 |
+
|
| 187 |
+
response_data = {}
|
| 188 |
+
retryable_error = False
|
| 189 |
+
|
| 190 |
+
retries = _exponential_backoff.ExponentialBackoff()
|
| 191 |
+
for _ in retries:
|
| 192 |
+
response = request(
|
| 193 |
+
method="POST", url=token_uri, headers=headers_to_use, body=body, **kwargs
|
| 194 |
+
)
|
| 195 |
+
response_body = (
|
| 196 |
+
response.data.decode("utf-8")
|
| 197 |
+
if hasattr(response.data, "decode")
|
| 198 |
+
else response.data
|
| 199 |
+
)
|
| 200 |
+
|
| 201 |
+
try:
|
| 202 |
+
# response_body should be a JSON
|
| 203 |
+
response_data = json.loads(response_body)
|
| 204 |
+
except ValueError:
|
| 205 |
+
response_data = response_body
|
| 206 |
+
|
| 207 |
+
if response.status == http_client.OK:
|
| 208 |
+
return True, response_data, None
|
| 209 |
+
|
| 210 |
+
retryable_error = _can_retry(
|
| 211 |
+
status_code=response.status, response_data=response_data
|
| 212 |
+
)
|
| 213 |
+
|
| 214 |
+
if not can_retry or not retryable_error:
|
| 215 |
+
return False, response_data, retryable_error
|
| 216 |
+
|
| 217 |
+
return False, response_data, retryable_error
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
def _token_endpoint_request(
|
| 221 |
+
request,
|
| 222 |
+
token_uri,
|
| 223 |
+
body,
|
| 224 |
+
access_token=None,
|
| 225 |
+
use_json=False,
|
| 226 |
+
can_retry=True,
|
| 227 |
+
headers=None,
|
| 228 |
+
**kwargs
|
| 229 |
+
):
|
| 230 |
+
"""Makes a request to the OAuth 2.0 authorization server's token endpoint.
|
| 231 |
+
|
| 232 |
+
Args:
|
| 233 |
+
request (google.auth.transport.Request): A callable used to make
|
| 234 |
+
HTTP requests.
|
| 235 |
+
token_uri (str): The OAuth 2.0 authorizations server's token endpoint
|
| 236 |
+
URI.
|
| 237 |
+
body (Mapping[str, str]): The parameters to send in the request body.
|
| 238 |
+
access_token (Optional(str)): The access token needed to make the request.
|
| 239 |
+
use_json (Optional(bool)): Use urlencoded format or json format for the
|
| 240 |
+
content type. The default value is False.
|
| 241 |
+
can_retry (bool): Enable or disable request retry behavior.
|
| 242 |
+
headers (Optional[Mapping[str, str]]): The headers for the request.
|
| 243 |
+
kwargs: Additional arguments passed on to the request method. The
|
| 244 |
+
kwargs will be passed to `requests.request` method, see:
|
| 245 |
+
https://docs.python-requests.org/en/latest/api/#requests.request.
|
| 246 |
+
For example, you can use `cert=("cert_pem_path", "key_pem_path")`
|
| 247 |
+
to set up client side SSL certificate, and use
|
| 248 |
+
`verify="ca_bundle_path"` to set up the CA certificates for sever
|
| 249 |
+
side SSL certificate verification.
|
| 250 |
+
|
| 251 |
+
Returns:
|
| 252 |
+
Mapping[str, str]: The JSON-decoded response data.
|
| 253 |
+
|
| 254 |
+
Raises:
|
| 255 |
+
google.auth.exceptions.RefreshError: If the token endpoint returned
|
| 256 |
+
an error.
|
| 257 |
+
"""
|
| 258 |
+
|
| 259 |
+
response_status_ok, response_data, retryable_error = _token_endpoint_request_no_throw(
|
| 260 |
+
request,
|
| 261 |
+
token_uri,
|
| 262 |
+
body,
|
| 263 |
+
access_token=access_token,
|
| 264 |
+
use_json=use_json,
|
| 265 |
+
can_retry=can_retry,
|
| 266 |
+
headers=headers,
|
| 267 |
+
**kwargs
|
| 268 |
+
)
|
| 269 |
+
if not response_status_ok:
|
| 270 |
+
_handle_error_response(response_data, retryable_error)
|
| 271 |
+
return response_data
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
def jwt_grant(request, token_uri, assertion, can_retry=True):
|
| 275 |
+
"""Implements the JWT Profile for OAuth 2.0 Authorization Grants.
|
| 276 |
+
|
| 277 |
+
For more details, see `rfc7523 section 4`_.
|
| 278 |
+
|
| 279 |
+
Args:
|
| 280 |
+
request (google.auth.transport.Request): A callable used to make
|
| 281 |
+
HTTP requests.
|
| 282 |
+
token_uri (str): The OAuth 2.0 authorizations server's token endpoint
|
| 283 |
+
URI.
|
| 284 |
+
assertion (str): The OAuth 2.0 assertion.
|
| 285 |
+
can_retry (bool): Enable or disable request retry behavior.
|
| 286 |
+
|
| 287 |
+
Returns:
|
| 288 |
+
Tuple[str, Optional[datetime], Mapping[str, str]]: The access token,
|
| 289 |
+
expiration, and additional data returned by the token endpoint.
|
| 290 |
+
|
| 291 |
+
Raises:
|
| 292 |
+
google.auth.exceptions.RefreshError: If the token endpoint returned
|
| 293 |
+
an error.
|
| 294 |
+
|
| 295 |
+
.. _rfc7523 section 4: https://tools.ietf.org/html/rfc7523#section-4
|
| 296 |
+
"""
|
| 297 |
+
body = {"assertion": assertion, "grant_type": _JWT_GRANT_TYPE}
|
| 298 |
+
|
| 299 |
+
response_data = _token_endpoint_request(
|
| 300 |
+
request,
|
| 301 |
+
token_uri,
|
| 302 |
+
body,
|
| 303 |
+
can_retry=can_retry,
|
| 304 |
+
headers={
|
| 305 |
+
metrics.API_CLIENT_HEADER: metrics.token_request_access_token_sa_assertion()
|
| 306 |
+
},
|
| 307 |
+
)
|
| 308 |
+
|
| 309 |
+
try:
|
| 310 |
+
access_token = response_data["access_token"]
|
| 311 |
+
except KeyError as caught_exc:
|
| 312 |
+
new_exc = exceptions.RefreshError(
|
| 313 |
+
"No access token in response.", response_data, retryable=False
|
| 314 |
+
)
|
| 315 |
+
raise new_exc from caught_exc
|
| 316 |
+
|
| 317 |
+
expiry = _parse_expiry(response_data)
|
| 318 |
+
|
| 319 |
+
return access_token, expiry, response_data
|
| 320 |
+
|
| 321 |
+
|
| 322 |
+
def call_iam_generate_id_token_endpoint(
|
| 323 |
+
request,
|
| 324 |
+
iam_id_token_endpoint,
|
| 325 |
+
signer_email,
|
| 326 |
+
audience,
|
| 327 |
+
access_token,
|
| 328 |
+
universe_domain=credentials.DEFAULT_UNIVERSE_DOMAIN,
|
| 329 |
+
):
|
| 330 |
+
"""Call iam.generateIdToken endpoint to get ID token.
|
| 331 |
+
|
| 332 |
+
Args:
|
| 333 |
+
request (google.auth.transport.Request): A callable used to make
|
| 334 |
+
HTTP requests.
|
| 335 |
+
iam_id_token_endpoint (str): The IAM ID token endpoint to use.
|
| 336 |
+
signer_email (str): The signer email used to form the IAM
|
| 337 |
+
generateIdToken endpoint.
|
| 338 |
+
audience (str): The audience for the ID token.
|
| 339 |
+
access_token (str): The access token used to call the IAM endpoint.
|
| 340 |
+
|
| 341 |
+
Returns:
|
| 342 |
+
Tuple[str, datetime]: The ID token and expiration.
|
| 343 |
+
"""
|
| 344 |
+
body = {"audience": audience, "includeEmail": "true", "useEmailAzp": "true"}
|
| 345 |
+
|
| 346 |
+
response_data = _token_endpoint_request(
|
| 347 |
+
request,
|
| 348 |
+
iam_id_token_endpoint.replace(
|
| 349 |
+
credentials.DEFAULT_UNIVERSE_DOMAIN, universe_domain
|
| 350 |
+
).format(signer_email),
|
| 351 |
+
body,
|
| 352 |
+
access_token=access_token,
|
| 353 |
+
use_json=True,
|
| 354 |
+
)
|
| 355 |
+
|
| 356 |
+
try:
|
| 357 |
+
id_token = response_data["token"]
|
| 358 |
+
except KeyError as caught_exc:
|
| 359 |
+
new_exc = exceptions.RefreshError(
|
| 360 |
+
"No ID token in response.", response_data, retryable=False
|
| 361 |
+
)
|
| 362 |
+
raise new_exc from caught_exc
|
| 363 |
+
|
| 364 |
+
payload = jwt.decode(id_token, verify=False)
|
| 365 |
+
expiry = datetime.datetime.utcfromtimestamp(payload["exp"])
|
| 366 |
+
|
| 367 |
+
return id_token, expiry
|
| 368 |
+
|
| 369 |
+
|
| 370 |
+
def id_token_jwt_grant(request, token_uri, assertion, can_retry=True):
|
| 371 |
+
"""Implements the JWT Profile for OAuth 2.0 Authorization Grants, but
|
| 372 |
+
requests an OpenID Connect ID Token instead of an access token.
|
| 373 |
+
|
| 374 |
+
This is a variant on the standard JWT Profile that is currently unique
|
| 375 |
+
to Google. This was added for the benefit of authenticating to services
|
| 376 |
+
that require ID Tokens instead of access tokens or JWT bearer tokens.
|
| 377 |
+
|
| 378 |
+
Args:
|
| 379 |
+
request (google.auth.transport.Request): A callable used to make
|
| 380 |
+
HTTP requests.
|
| 381 |
+
token_uri (str): The OAuth 2.0 authorization server's token endpoint
|
| 382 |
+
URI.
|
| 383 |
+
assertion (str): JWT token signed by a service account. The token's
|
| 384 |
+
payload must include a ``target_audience`` claim.
|
| 385 |
+
can_retry (bool): Enable or disable request retry behavior.
|
| 386 |
+
|
| 387 |
+
Returns:
|
| 388 |
+
Tuple[str, Optional[datetime], Mapping[str, str]]:
|
| 389 |
+
The (encoded) Open ID Connect ID Token, expiration, and additional
|
| 390 |
+
data returned by the endpoint.
|
| 391 |
+
|
| 392 |
+
Raises:
|
| 393 |
+
google.auth.exceptions.RefreshError: If the token endpoint returned
|
| 394 |
+
an error.
|
| 395 |
+
"""
|
| 396 |
+
body = {"assertion": assertion, "grant_type": _JWT_GRANT_TYPE}
|
| 397 |
+
|
| 398 |
+
response_data = _token_endpoint_request(
|
| 399 |
+
request,
|
| 400 |
+
token_uri,
|
| 401 |
+
body,
|
| 402 |
+
can_retry=can_retry,
|
| 403 |
+
headers={
|
| 404 |
+
metrics.API_CLIENT_HEADER: metrics.token_request_id_token_sa_assertion()
|
| 405 |
+
},
|
| 406 |
+
)
|
| 407 |
+
|
| 408 |
+
try:
|
| 409 |
+
id_token = response_data["id_token"]
|
| 410 |
+
except KeyError as caught_exc:
|
| 411 |
+
new_exc = exceptions.RefreshError(
|
| 412 |
+
"No ID token in response.", response_data, retryable=False
|
| 413 |
+
)
|
| 414 |
+
raise new_exc from caught_exc
|
| 415 |
+
|
| 416 |
+
payload = jwt.decode(id_token, verify=False)
|
| 417 |
+
expiry = datetime.datetime.utcfromtimestamp(payload["exp"])
|
| 418 |
+
|
| 419 |
+
return id_token, expiry, response_data
|
| 420 |
+
|
| 421 |
+
|
| 422 |
+
def _handle_refresh_grant_response(response_data, refresh_token):
|
| 423 |
+
"""Extract tokens from refresh grant response.
|
| 424 |
+
|
| 425 |
+
Args:
|
| 426 |
+
response_data (Mapping[str, str]): Refresh grant response data.
|
| 427 |
+
refresh_token (str): Current refresh token.
|
| 428 |
+
|
| 429 |
+
Returns:
|
| 430 |
+
Tuple[str, str, Optional[datetime], Mapping[str, str]]: The access token,
|
| 431 |
+
refresh token, expiration, and additional data returned by the token
|
| 432 |
+
endpoint. If response_data doesn't have refresh token, then the current
|
| 433 |
+
refresh token will be returned.
|
| 434 |
+
|
| 435 |
+
Raises:
|
| 436 |
+
google.auth.exceptions.RefreshError: If the token endpoint returned
|
| 437 |
+
an error.
|
| 438 |
+
"""
|
| 439 |
+
try:
|
| 440 |
+
access_token = response_data["access_token"]
|
| 441 |
+
except KeyError as caught_exc:
|
| 442 |
+
new_exc = exceptions.RefreshError(
|
| 443 |
+
"No access token in response.", response_data, retryable=False
|
| 444 |
+
)
|
| 445 |
+
raise new_exc from caught_exc
|
| 446 |
+
|
| 447 |
+
refresh_token = response_data.get("refresh_token", refresh_token)
|
| 448 |
+
expiry = _parse_expiry(response_data)
|
| 449 |
+
|
| 450 |
+
return access_token, refresh_token, expiry, response_data
|
| 451 |
+
|
| 452 |
+
|
| 453 |
+
def refresh_grant(
|
| 454 |
+
request,
|
| 455 |
+
token_uri,
|
| 456 |
+
refresh_token,
|
| 457 |
+
client_id,
|
| 458 |
+
client_secret,
|
| 459 |
+
scopes=None,
|
| 460 |
+
rapt_token=None,
|
| 461 |
+
can_retry=True,
|
| 462 |
+
):
|
| 463 |
+
"""Implements the OAuth 2.0 refresh token grant.
|
| 464 |
+
|
| 465 |
+
For more details, see `rfc678 section 6`_.
|
| 466 |
+
|
| 467 |
+
Args:
|
| 468 |
+
request (google.auth.transport.Request): A callable used to make
|
| 469 |
+
HTTP requests.
|
| 470 |
+
token_uri (str): The OAuth 2.0 authorizations server's token endpoint
|
| 471 |
+
URI.
|
| 472 |
+
refresh_token (str): The refresh token to use to get a new access
|
| 473 |
+
token.
|
| 474 |
+
client_id (str): The OAuth 2.0 application's client ID.
|
| 475 |
+
client_secret (str): The Oauth 2.0 appliaction's client secret.
|
| 476 |
+
scopes (Optional(Sequence[str])): Scopes to request. If present, all
|
| 477 |
+
scopes must be authorized for the refresh token. Useful if refresh
|
| 478 |
+
token has a wild card scope (e.g.
|
| 479 |
+
'https://www.googleapis.com/auth/any-api').
|
| 480 |
+
rapt_token (Optional(str)): The reauth Proof Token.
|
| 481 |
+
can_retry (bool): Enable or disable request retry behavior.
|
| 482 |
+
|
| 483 |
+
Returns:
|
| 484 |
+
Tuple[str, str, Optional[datetime], Mapping[str, str]]: The access
|
| 485 |
+
token, new or current refresh token, expiration, and additional data
|
| 486 |
+
returned by the token endpoint.
|
| 487 |
+
|
| 488 |
+
Raises:
|
| 489 |
+
google.auth.exceptions.RefreshError: If the token endpoint returned
|
| 490 |
+
an error.
|
| 491 |
+
|
| 492 |
+
.. _rfc6748 section 6: https://tools.ietf.org/html/rfc6749#section-6
|
| 493 |
+
"""
|
| 494 |
+
body = {
|
| 495 |
+
"grant_type": _REFRESH_GRANT_TYPE,
|
| 496 |
+
"client_id": client_id,
|
| 497 |
+
"client_secret": client_secret,
|
| 498 |
+
"refresh_token": refresh_token,
|
| 499 |
+
}
|
| 500 |
+
if scopes:
|
| 501 |
+
body["scope"] = " ".join(scopes)
|
| 502 |
+
if rapt_token:
|
| 503 |
+
body["rapt"] = rapt_token
|
| 504 |
+
|
| 505 |
+
response_data = _token_endpoint_request(
|
| 506 |
+
request, token_uri, body, can_retry=can_retry
|
| 507 |
+
)
|
| 508 |
+
return _handle_refresh_grant_response(response_data, refresh_token)
|
.venv/lib/python3.11/site-packages/google/oauth2/_client_async.py
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2020 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""OAuth 2.0 async client.
|
| 16 |
+
|
| 17 |
+
This is a client for interacting with an OAuth 2.0 authorization server's
|
| 18 |
+
token endpoint.
|
| 19 |
+
|
| 20 |
+
For more information about the token endpoint, see
|
| 21 |
+
`Section 3.1 of rfc6749`_
|
| 22 |
+
|
| 23 |
+
.. _Section 3.1 of rfc6749: https://tools.ietf.org/html/rfc6749#section-3.2
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
import datetime
|
| 27 |
+
import http.client as http_client
|
| 28 |
+
import json
|
| 29 |
+
import urllib
|
| 30 |
+
|
| 31 |
+
from google.auth import _exponential_backoff
|
| 32 |
+
from google.auth import exceptions
|
| 33 |
+
from google.auth import jwt
|
| 34 |
+
from google.oauth2 import _client as client
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
async def _token_endpoint_request_no_throw(
|
| 38 |
+
request, token_uri, body, access_token=None, use_json=False, can_retry=True
|
| 39 |
+
):
|
| 40 |
+
"""Makes a request to the OAuth 2.0 authorization server's token endpoint.
|
| 41 |
+
This function doesn't throw on response errors.
|
| 42 |
+
|
| 43 |
+
Args:
|
| 44 |
+
request (google.auth.transport.Request): A callable used to make
|
| 45 |
+
HTTP requests.
|
| 46 |
+
token_uri (str): The OAuth 2.0 authorizations server's token endpoint
|
| 47 |
+
URI.
|
| 48 |
+
body (Mapping[str, str]): The parameters to send in the request body.
|
| 49 |
+
access_token (Optional(str)): The access token needed to make the request.
|
| 50 |
+
use_json (Optional(bool)): Use urlencoded format or json format for the
|
| 51 |
+
content type. The default value is False.
|
| 52 |
+
can_retry (bool): Enable or disable request retry behavior.
|
| 53 |
+
|
| 54 |
+
Returns:
|
| 55 |
+
Tuple(bool, Mapping[str, str], Optional[bool]): A boolean indicating
|
| 56 |
+
if the request is successful, a mapping for the JSON-decoded response
|
| 57 |
+
data and in the case of an error a boolean indicating if the error
|
| 58 |
+
is retryable.
|
| 59 |
+
"""
|
| 60 |
+
if use_json:
|
| 61 |
+
headers = {"Content-Type": client._JSON_CONTENT_TYPE}
|
| 62 |
+
body = json.dumps(body).encode("utf-8")
|
| 63 |
+
else:
|
| 64 |
+
headers = {"Content-Type": client._URLENCODED_CONTENT_TYPE}
|
| 65 |
+
body = urllib.parse.urlencode(body).encode("utf-8")
|
| 66 |
+
|
| 67 |
+
if access_token:
|
| 68 |
+
headers["Authorization"] = "Bearer {}".format(access_token)
|
| 69 |
+
|
| 70 |
+
response_data = {}
|
| 71 |
+
retryable_error = False
|
| 72 |
+
|
| 73 |
+
retries = _exponential_backoff.ExponentialBackoff()
|
| 74 |
+
for _ in retries:
|
| 75 |
+
response = await request(
|
| 76 |
+
method="POST", url=token_uri, headers=headers, body=body
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
# Using data.read() resulted in zlib decompression errors. This may require future investigation.
|
| 80 |
+
response_body1 = await response.content()
|
| 81 |
+
|
| 82 |
+
response_body = (
|
| 83 |
+
response_body1.decode("utf-8")
|
| 84 |
+
if hasattr(response_body1, "decode")
|
| 85 |
+
else response_body1
|
| 86 |
+
)
|
| 87 |
+
|
| 88 |
+
try:
|
| 89 |
+
response_data = json.loads(response_body)
|
| 90 |
+
except ValueError:
|
| 91 |
+
response_data = response_body
|
| 92 |
+
|
| 93 |
+
if response.status == http_client.OK:
|
| 94 |
+
return True, response_data, None
|
| 95 |
+
|
| 96 |
+
retryable_error = client._can_retry(
|
| 97 |
+
status_code=response.status, response_data=response_data
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
if not can_retry or not retryable_error:
|
| 101 |
+
return False, response_data, retryable_error
|
| 102 |
+
|
| 103 |
+
return False, response_data, retryable_error
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
async def _token_endpoint_request(
|
| 107 |
+
request, token_uri, body, access_token=None, use_json=False, can_retry=True
|
| 108 |
+
):
|
| 109 |
+
"""Makes a request to the OAuth 2.0 authorization server's token endpoint.
|
| 110 |
+
|
| 111 |
+
Args:
|
| 112 |
+
request (google.auth.transport.Request): A callable used to make
|
| 113 |
+
HTTP requests.
|
| 114 |
+
token_uri (str): The OAuth 2.0 authorizations server's token endpoint
|
| 115 |
+
URI.
|
| 116 |
+
body (Mapping[str, str]): The parameters to send in the request body.
|
| 117 |
+
access_token (Optional(str)): The access token needed to make the request.
|
| 118 |
+
use_json (Optional(bool)): Use urlencoded format or json format for the
|
| 119 |
+
content type. The default value is False.
|
| 120 |
+
can_retry (bool): Enable or disable request retry behavior.
|
| 121 |
+
|
| 122 |
+
Returns:
|
| 123 |
+
Mapping[str, str]: The JSON-decoded response data.
|
| 124 |
+
|
| 125 |
+
Raises:
|
| 126 |
+
google.auth.exceptions.RefreshError: If the token endpoint returned
|
| 127 |
+
an error.
|
| 128 |
+
"""
|
| 129 |
+
|
| 130 |
+
response_status_ok, response_data, retryable_error = await _token_endpoint_request_no_throw(
|
| 131 |
+
request,
|
| 132 |
+
token_uri,
|
| 133 |
+
body,
|
| 134 |
+
access_token=access_token,
|
| 135 |
+
use_json=use_json,
|
| 136 |
+
can_retry=can_retry,
|
| 137 |
+
)
|
| 138 |
+
if not response_status_ok:
|
| 139 |
+
client._handle_error_response(response_data, retryable_error)
|
| 140 |
+
return response_data
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
async def jwt_grant(request, token_uri, assertion, can_retry=True):
|
| 144 |
+
"""Implements the JWT Profile for OAuth 2.0 Authorization Grants.
|
| 145 |
+
|
| 146 |
+
For more details, see `rfc7523 section 4`_.
|
| 147 |
+
|
| 148 |
+
Args:
|
| 149 |
+
request (google.auth.transport.Request): A callable used to make
|
| 150 |
+
HTTP requests.
|
| 151 |
+
token_uri (str): The OAuth 2.0 authorizations server's token endpoint
|
| 152 |
+
URI.
|
| 153 |
+
assertion (str): The OAuth 2.0 assertion.
|
| 154 |
+
can_retry (bool): Enable or disable request retry behavior.
|
| 155 |
+
|
| 156 |
+
Returns:
|
| 157 |
+
Tuple[str, Optional[datetime], Mapping[str, str]]: The access token,
|
| 158 |
+
expiration, and additional data returned by the token endpoint.
|
| 159 |
+
|
| 160 |
+
Raises:
|
| 161 |
+
google.auth.exceptions.RefreshError: If the token endpoint returned
|
| 162 |
+
an error.
|
| 163 |
+
|
| 164 |
+
.. _rfc7523 section 4: https://tools.ietf.org/html/rfc7523#section-4
|
| 165 |
+
"""
|
| 166 |
+
body = {"assertion": assertion, "grant_type": client._JWT_GRANT_TYPE}
|
| 167 |
+
|
| 168 |
+
response_data = await _token_endpoint_request(
|
| 169 |
+
request, token_uri, body, can_retry=can_retry
|
| 170 |
+
)
|
| 171 |
+
|
| 172 |
+
try:
|
| 173 |
+
access_token = response_data["access_token"]
|
| 174 |
+
except KeyError as caught_exc:
|
| 175 |
+
new_exc = exceptions.RefreshError(
|
| 176 |
+
"No access token in response.", response_data, retryable=False
|
| 177 |
+
)
|
| 178 |
+
raise new_exc from caught_exc
|
| 179 |
+
|
| 180 |
+
expiry = client._parse_expiry(response_data)
|
| 181 |
+
|
| 182 |
+
return access_token, expiry, response_data
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
async def id_token_jwt_grant(request, token_uri, assertion, can_retry=True):
|
| 186 |
+
"""Implements the JWT Profile for OAuth 2.0 Authorization Grants, but
|
| 187 |
+
requests an OpenID Connect ID Token instead of an access token.
|
| 188 |
+
|
| 189 |
+
This is a variant on the standard JWT Profile that is currently unique
|
| 190 |
+
to Google. This was added for the benefit of authenticating to services
|
| 191 |
+
that require ID Tokens instead of access tokens or JWT bearer tokens.
|
| 192 |
+
|
| 193 |
+
Args:
|
| 194 |
+
request (google.auth.transport.Request): A callable used to make
|
| 195 |
+
HTTP requests.
|
| 196 |
+
token_uri (str): The OAuth 2.0 authorization server's token endpoint
|
| 197 |
+
URI.
|
| 198 |
+
assertion (str): JWT token signed by a service account. The token's
|
| 199 |
+
payload must include a ``target_audience`` claim.
|
| 200 |
+
can_retry (bool): Enable or disable request retry behavior.
|
| 201 |
+
|
| 202 |
+
Returns:
|
| 203 |
+
Tuple[str, Optional[datetime], Mapping[str, str]]:
|
| 204 |
+
The (encoded) Open ID Connect ID Token, expiration, and additional
|
| 205 |
+
data returned by the endpoint.
|
| 206 |
+
|
| 207 |
+
Raises:
|
| 208 |
+
google.auth.exceptions.RefreshError: If the token endpoint returned
|
| 209 |
+
an error.
|
| 210 |
+
"""
|
| 211 |
+
body = {"assertion": assertion, "grant_type": client._JWT_GRANT_TYPE}
|
| 212 |
+
|
| 213 |
+
response_data = await _token_endpoint_request(
|
| 214 |
+
request, token_uri, body, can_retry=can_retry
|
| 215 |
+
)
|
| 216 |
+
|
| 217 |
+
try:
|
| 218 |
+
id_token = response_data["id_token"]
|
| 219 |
+
except KeyError as caught_exc:
|
| 220 |
+
new_exc = exceptions.RefreshError(
|
| 221 |
+
"No ID token in response.", response_data, retryable=False
|
| 222 |
+
)
|
| 223 |
+
raise new_exc from caught_exc
|
| 224 |
+
|
| 225 |
+
payload = jwt.decode(id_token, verify=False)
|
| 226 |
+
expiry = datetime.datetime.utcfromtimestamp(payload["exp"])
|
| 227 |
+
|
| 228 |
+
return id_token, expiry, response_data
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
async def refresh_grant(
|
| 232 |
+
request,
|
| 233 |
+
token_uri,
|
| 234 |
+
refresh_token,
|
| 235 |
+
client_id,
|
| 236 |
+
client_secret,
|
| 237 |
+
scopes=None,
|
| 238 |
+
rapt_token=None,
|
| 239 |
+
can_retry=True,
|
| 240 |
+
):
|
| 241 |
+
"""Implements the OAuth 2.0 refresh token grant.
|
| 242 |
+
|
| 243 |
+
For more details, see `rfc678 section 6`_.
|
| 244 |
+
|
| 245 |
+
Args:
|
| 246 |
+
request (google.auth.transport.Request): A callable used to make
|
| 247 |
+
HTTP requests.
|
| 248 |
+
token_uri (str): The OAuth 2.0 authorizations server's token endpoint
|
| 249 |
+
URI.
|
| 250 |
+
refresh_token (str): The refresh token to use to get a new access
|
| 251 |
+
token.
|
| 252 |
+
client_id (str): The OAuth 2.0 application's client ID.
|
| 253 |
+
client_secret (str): The Oauth 2.0 appliaction's client secret.
|
| 254 |
+
scopes (Optional(Sequence[str])): Scopes to request. If present, all
|
| 255 |
+
scopes must be authorized for the refresh token. Useful if refresh
|
| 256 |
+
token has a wild card scope (e.g.
|
| 257 |
+
'https://www.googleapis.com/auth/any-api').
|
| 258 |
+
rapt_token (Optional(str)): The reauth Proof Token.
|
| 259 |
+
can_retry (bool): Enable or disable request retry behavior.
|
| 260 |
+
|
| 261 |
+
Returns:
|
| 262 |
+
Tuple[str, Optional[str], Optional[datetime], Mapping[str, str]]: The
|
| 263 |
+
access token, new or current refresh token, expiration, and additional data
|
| 264 |
+
returned by the token endpoint.
|
| 265 |
+
|
| 266 |
+
Raises:
|
| 267 |
+
google.auth.exceptions.RefreshError: If the token endpoint returned
|
| 268 |
+
an error.
|
| 269 |
+
|
| 270 |
+
.. _rfc6748 section 6: https://tools.ietf.org/html/rfc6749#section-6
|
| 271 |
+
"""
|
| 272 |
+
body = {
|
| 273 |
+
"grant_type": client._REFRESH_GRANT_TYPE,
|
| 274 |
+
"client_id": client_id,
|
| 275 |
+
"client_secret": client_secret,
|
| 276 |
+
"refresh_token": refresh_token,
|
| 277 |
+
}
|
| 278 |
+
if scopes:
|
| 279 |
+
body["scope"] = " ".join(scopes)
|
| 280 |
+
if rapt_token:
|
| 281 |
+
body["rapt"] = rapt_token
|
| 282 |
+
|
| 283 |
+
response_data = await _token_endpoint_request(
|
| 284 |
+
request, token_uri, body, can_retry=can_retry
|
| 285 |
+
)
|
| 286 |
+
return client._handle_refresh_grant_response(response_data, refresh_token)
|
.venv/lib/python3.11/site-packages/google/oauth2/_credentials_async.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2020 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""OAuth 2.0 Async Credentials.
|
| 16 |
+
|
| 17 |
+
This module provides credentials based on OAuth 2.0 access and refresh tokens.
|
| 18 |
+
These credentials usually access resources on behalf of a user (resource
|
| 19 |
+
owner).
|
| 20 |
+
|
| 21 |
+
Specifically, this is intended to use access tokens acquired using the
|
| 22 |
+
`Authorization Code grant`_ and can refresh those tokens using a
|
| 23 |
+
optional `refresh token`_.
|
| 24 |
+
|
| 25 |
+
Obtaining the initial access and refresh token is outside of the scope of this
|
| 26 |
+
module. Consult `rfc6749 section 4.1`_ for complete details on the
|
| 27 |
+
Authorization Code grant flow.
|
| 28 |
+
|
| 29 |
+
.. _Authorization Code grant: https://tools.ietf.org/html/rfc6749#section-1.3.1
|
| 30 |
+
.. _refresh token: https://tools.ietf.org/html/rfc6749#section-6
|
| 31 |
+
.. _rfc6749 section 4.1: https://tools.ietf.org/html/rfc6749#section-4.1
|
| 32 |
+
"""
|
| 33 |
+
|
| 34 |
+
from google.auth import _credentials_async as credentials
|
| 35 |
+
from google.auth import _helpers
|
| 36 |
+
from google.auth import exceptions
|
| 37 |
+
from google.oauth2 import _reauth_async as reauth
|
| 38 |
+
from google.oauth2 import credentials as oauth2_credentials
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class Credentials(oauth2_credentials.Credentials):
|
| 42 |
+
"""Credentials using OAuth 2.0 access and refresh tokens.
|
| 43 |
+
|
| 44 |
+
The credentials are considered immutable. If you want to modify the
|
| 45 |
+
quota project, use :meth:`with_quota_project` or ::
|
| 46 |
+
|
| 47 |
+
credentials = credentials.with_quota_project('myproject-123)
|
| 48 |
+
"""
|
| 49 |
+
|
| 50 |
+
@_helpers.copy_docstring(credentials.Credentials)
|
| 51 |
+
async def refresh(self, request):
|
| 52 |
+
if (
|
| 53 |
+
self._refresh_token is None
|
| 54 |
+
or self._token_uri is None
|
| 55 |
+
or self._client_id is None
|
| 56 |
+
or self._client_secret is None
|
| 57 |
+
):
|
| 58 |
+
raise exceptions.RefreshError(
|
| 59 |
+
"The credentials do not contain the necessary fields need to "
|
| 60 |
+
"refresh the access token. You must specify refresh_token, "
|
| 61 |
+
"token_uri, client_id, and client_secret."
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
(
|
| 65 |
+
access_token,
|
| 66 |
+
refresh_token,
|
| 67 |
+
expiry,
|
| 68 |
+
grant_response,
|
| 69 |
+
rapt_token,
|
| 70 |
+
) = await reauth.refresh_grant(
|
| 71 |
+
request,
|
| 72 |
+
self._token_uri,
|
| 73 |
+
self._refresh_token,
|
| 74 |
+
self._client_id,
|
| 75 |
+
self._client_secret,
|
| 76 |
+
scopes=self._scopes,
|
| 77 |
+
rapt_token=self._rapt_token,
|
| 78 |
+
enable_reauth_refresh=self._enable_reauth_refresh,
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
self.token = access_token
|
| 82 |
+
self.expiry = expiry
|
| 83 |
+
self._refresh_token = refresh_token
|
| 84 |
+
self._id_token = grant_response.get("id_token")
|
| 85 |
+
self._rapt_token = rapt_token
|
| 86 |
+
|
| 87 |
+
if self._scopes and "scope" in grant_response:
|
| 88 |
+
requested_scopes = frozenset(self._scopes)
|
| 89 |
+
granted_scopes = frozenset(grant_response["scope"].split())
|
| 90 |
+
scopes_requested_but_not_granted = requested_scopes - granted_scopes
|
| 91 |
+
if scopes_requested_but_not_granted:
|
| 92 |
+
raise exceptions.RefreshError(
|
| 93 |
+
"Not all requested scopes were granted by the "
|
| 94 |
+
"authorization server, missing scopes {}.".format(
|
| 95 |
+
", ".join(scopes_requested_but_not_granted)
|
| 96 |
+
)
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
@_helpers.copy_docstring(credentials.Credentials)
|
| 100 |
+
async def before_request(self, request, method, url, headers):
|
| 101 |
+
if not self.valid:
|
| 102 |
+
await self.refresh(request)
|
| 103 |
+
self.apply(headers)
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
class UserAccessTokenCredentials(oauth2_credentials.UserAccessTokenCredentials):
|
| 107 |
+
"""Access token credentials for user account.
|
| 108 |
+
|
| 109 |
+
Obtain the access token for a given user account or the current active
|
| 110 |
+
user account with the ``gcloud auth print-access-token`` command.
|
| 111 |
+
|
| 112 |
+
Args:
|
| 113 |
+
account (Optional[str]): Account to get the access token for. If not
|
| 114 |
+
specified, the current active account will be used.
|
| 115 |
+
quota_project_id (Optional[str]): The project ID used for quota
|
| 116 |
+
and billing.
|
| 117 |
+
|
| 118 |
+
"""
|
.venv/lib/python3.11/site-packages/google/oauth2/_id_token_async.py
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2020 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""Google ID Token helpers.
|
| 16 |
+
|
| 17 |
+
Provides support for verifying `OpenID Connect ID Tokens`_, especially ones
|
| 18 |
+
generated by Google infrastructure.
|
| 19 |
+
|
| 20 |
+
To parse and verify an ID Token issued by Google's OAuth 2.0 authorization
|
| 21 |
+
server use :func:`verify_oauth2_token`. To verify an ID Token issued by
|
| 22 |
+
Firebase, use :func:`verify_firebase_token`.
|
| 23 |
+
|
| 24 |
+
A general purpose ID Token verifier is available as :func:`verify_token`.
|
| 25 |
+
|
| 26 |
+
Example::
|
| 27 |
+
|
| 28 |
+
from google.oauth2 import _id_token_async
|
| 29 |
+
from google.auth.transport import aiohttp_requests
|
| 30 |
+
|
| 31 |
+
request = aiohttp_requests.Request()
|
| 32 |
+
|
| 33 |
+
id_info = await _id_token_async.verify_oauth2_token(
|
| 34 |
+
token, request, 'my-client-id.example.com')
|
| 35 |
+
|
| 36 |
+
if id_info['iss'] != 'https://accounts.google.com':
|
| 37 |
+
raise ValueError('Wrong issuer.')
|
| 38 |
+
|
| 39 |
+
userid = id_info['sub']
|
| 40 |
+
|
| 41 |
+
By default, this will re-fetch certificates for each verification. Because
|
| 42 |
+
Google's public keys are only changed infrequently (on the order of once per
|
| 43 |
+
day), you may wish to take advantage of caching to reduce latency and the
|
| 44 |
+
potential for network errors. This can be accomplished using an external
|
| 45 |
+
library like `CacheControl`_ to create a cache-aware
|
| 46 |
+
:class:`google.auth.transport.Request`::
|
| 47 |
+
|
| 48 |
+
import cachecontrol
|
| 49 |
+
import google.auth.transport.requests
|
| 50 |
+
import requests
|
| 51 |
+
|
| 52 |
+
session = requests.session()
|
| 53 |
+
cached_session = cachecontrol.CacheControl(session)
|
| 54 |
+
request = google.auth.transport.requests.Request(session=cached_session)
|
| 55 |
+
|
| 56 |
+
.. _OpenID Connect ID Token:
|
| 57 |
+
http://openid.net/specs/openid-connect-core-1_0.html#IDToken
|
| 58 |
+
.. _CacheControl: https://cachecontrol.readthedocs.io
|
| 59 |
+
"""
|
| 60 |
+
|
| 61 |
+
import http.client as http_client
|
| 62 |
+
import json
|
| 63 |
+
import os
|
| 64 |
+
|
| 65 |
+
from google.auth import environment_vars
|
| 66 |
+
from google.auth import exceptions
|
| 67 |
+
from google.auth import jwt
|
| 68 |
+
from google.auth.transport import requests
|
| 69 |
+
from google.oauth2 import id_token as sync_id_token
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
async def _fetch_certs(request, certs_url):
|
| 73 |
+
"""Fetches certificates.
|
| 74 |
+
|
| 75 |
+
Google-style cerificate endpoints return JSON in the format of
|
| 76 |
+
``{'key id': 'x509 certificate'}``.
|
| 77 |
+
|
| 78 |
+
Args:
|
| 79 |
+
request (google.auth.transport.Request): The object used to make
|
| 80 |
+
HTTP requests. This must be an aiohttp request.
|
| 81 |
+
certs_url (str): The certificate endpoint URL.
|
| 82 |
+
|
| 83 |
+
Returns:
|
| 84 |
+
Mapping[str, str]: A mapping of public key ID to x.509 certificate
|
| 85 |
+
data.
|
| 86 |
+
"""
|
| 87 |
+
response = await request(certs_url, method="GET")
|
| 88 |
+
|
| 89 |
+
if response.status != http_client.OK:
|
| 90 |
+
raise exceptions.TransportError(
|
| 91 |
+
"Could not fetch certificates at {}".format(certs_url)
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
data = await response.content()
|
| 95 |
+
|
| 96 |
+
return json.loads(data)
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
async def verify_token(
|
| 100 |
+
id_token,
|
| 101 |
+
request,
|
| 102 |
+
audience=None,
|
| 103 |
+
certs_url=sync_id_token._GOOGLE_OAUTH2_CERTS_URL,
|
| 104 |
+
clock_skew_in_seconds=0,
|
| 105 |
+
):
|
| 106 |
+
"""Verifies an ID token and returns the decoded token.
|
| 107 |
+
|
| 108 |
+
Args:
|
| 109 |
+
id_token (Union[str, bytes]): The encoded token.
|
| 110 |
+
request (google.auth.transport.Request): The object used to make
|
| 111 |
+
HTTP requests. This must be an aiohttp request.
|
| 112 |
+
audience (str): The audience that this token is intended for. If None
|
| 113 |
+
then the audience is not verified.
|
| 114 |
+
certs_url (str): The URL that specifies the certificates to use to
|
| 115 |
+
verify the token. This URL should return JSON in the format of
|
| 116 |
+
``{'key id': 'x509 certificate'}``.
|
| 117 |
+
clock_skew_in_seconds (int): The clock skew used for `iat` and `exp`
|
| 118 |
+
validation.
|
| 119 |
+
|
| 120 |
+
Returns:
|
| 121 |
+
Mapping[str, Any]: The decoded token.
|
| 122 |
+
"""
|
| 123 |
+
certs = await _fetch_certs(request, certs_url)
|
| 124 |
+
|
| 125 |
+
return jwt.decode(
|
| 126 |
+
id_token,
|
| 127 |
+
certs=certs,
|
| 128 |
+
audience=audience,
|
| 129 |
+
clock_skew_in_seconds=clock_skew_in_seconds,
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
async def verify_oauth2_token(
|
| 134 |
+
id_token, request, audience=None, clock_skew_in_seconds=0
|
| 135 |
+
):
|
| 136 |
+
"""Verifies an ID Token issued by Google's OAuth 2.0 authorization server.
|
| 137 |
+
|
| 138 |
+
Args:
|
| 139 |
+
id_token (Union[str, bytes]): The encoded token.
|
| 140 |
+
request (google.auth.transport.Request): The object used to make
|
| 141 |
+
HTTP requests. This must be an aiohttp request.
|
| 142 |
+
audience (str): The audience that this token is intended for. This is
|
| 143 |
+
typically your application's OAuth 2.0 client ID. If None then the
|
| 144 |
+
audience is not verified.
|
| 145 |
+
clock_skew_in_seconds (int): The clock skew used for `iat` and `exp`
|
| 146 |
+
validation.
|
| 147 |
+
|
| 148 |
+
Returns:
|
| 149 |
+
Mapping[str, Any]: The decoded token.
|
| 150 |
+
|
| 151 |
+
Raises:
|
| 152 |
+
exceptions.GoogleAuthError: If the issuer is invalid.
|
| 153 |
+
"""
|
| 154 |
+
idinfo = await verify_token(
|
| 155 |
+
id_token,
|
| 156 |
+
request,
|
| 157 |
+
audience=audience,
|
| 158 |
+
certs_url=sync_id_token._GOOGLE_OAUTH2_CERTS_URL,
|
| 159 |
+
clock_skew_in_seconds=clock_skew_in_seconds,
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
+
if idinfo["iss"] not in sync_id_token._GOOGLE_ISSUERS:
|
| 163 |
+
raise exceptions.GoogleAuthError(
|
| 164 |
+
"Wrong issuer. 'iss' should be one of the following: {}".format(
|
| 165 |
+
sync_id_token._GOOGLE_ISSUERS
|
| 166 |
+
)
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
return idinfo
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
async def verify_firebase_token(
|
| 173 |
+
id_token, request, audience=None, clock_skew_in_seconds=0
|
| 174 |
+
):
|
| 175 |
+
"""Verifies an ID Token issued by Firebase Authentication.
|
| 176 |
+
|
| 177 |
+
Args:
|
| 178 |
+
id_token (Union[str, bytes]): The encoded token.
|
| 179 |
+
request (google.auth.transport.Request): The object used to make
|
| 180 |
+
HTTP requests. This must be an aiohttp request.
|
| 181 |
+
audience (str): The audience that this token is intended for. This is
|
| 182 |
+
typically your Firebase application ID. If None then the audience
|
| 183 |
+
is not verified.
|
| 184 |
+
clock_skew_in_seconds (int): The clock skew used for `iat` and `exp`
|
| 185 |
+
validation.
|
| 186 |
+
|
| 187 |
+
Returns:
|
| 188 |
+
Mapping[str, Any]: The decoded token.
|
| 189 |
+
"""
|
| 190 |
+
return await verify_token(
|
| 191 |
+
id_token,
|
| 192 |
+
request,
|
| 193 |
+
audience=audience,
|
| 194 |
+
certs_url=sync_id_token._GOOGLE_APIS_CERTS_URL,
|
| 195 |
+
clock_skew_in_seconds=clock_skew_in_seconds,
|
| 196 |
+
)
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
async def fetch_id_token(request, audience):
|
| 200 |
+
"""Fetch the ID Token from the current environment.
|
| 201 |
+
|
| 202 |
+
This function acquires ID token from the environment in the following order.
|
| 203 |
+
See https://google.aip.dev/auth/4110.
|
| 204 |
+
|
| 205 |
+
1. If the environment variable ``GOOGLE_APPLICATION_CREDENTIALS`` is set
|
| 206 |
+
to the path of a valid service account JSON file, then ID token is
|
| 207 |
+
acquired using this service account credentials.
|
| 208 |
+
2. If the application is running in Compute Engine, App Engine or Cloud Run,
|
| 209 |
+
then the ID token are obtained from the metadata server.
|
| 210 |
+
3. If metadata server doesn't exist and no valid service account credentials
|
| 211 |
+
are found, :class:`~google.auth.exceptions.DefaultCredentialsError` will
|
| 212 |
+
be raised.
|
| 213 |
+
|
| 214 |
+
Example::
|
| 215 |
+
|
| 216 |
+
import google.oauth2._id_token_async
|
| 217 |
+
import google.auth.transport.aiohttp_requests
|
| 218 |
+
|
| 219 |
+
request = google.auth.transport.aiohttp_requests.Request()
|
| 220 |
+
target_audience = "https://pubsub.googleapis.com"
|
| 221 |
+
|
| 222 |
+
id_token = await google.oauth2._id_token_async.fetch_id_token(request, target_audience)
|
| 223 |
+
|
| 224 |
+
Args:
|
| 225 |
+
request (google.auth.transport.aiohttp_requests.Request): A callable used to make
|
| 226 |
+
HTTP requests.
|
| 227 |
+
audience (str): The audience that this ID token is intended for.
|
| 228 |
+
|
| 229 |
+
Returns:
|
| 230 |
+
str: The ID token.
|
| 231 |
+
|
| 232 |
+
Raises:
|
| 233 |
+
~google.auth.exceptions.DefaultCredentialsError:
|
| 234 |
+
If metadata server doesn't exist and no valid service account
|
| 235 |
+
credentials are found.
|
| 236 |
+
"""
|
| 237 |
+
# 1. Try to get credentials from the GOOGLE_APPLICATION_CREDENTIALS environment
|
| 238 |
+
# variable.
|
| 239 |
+
credentials_filename = os.environ.get(environment_vars.CREDENTIALS)
|
| 240 |
+
if credentials_filename:
|
| 241 |
+
if not (
|
| 242 |
+
os.path.exists(credentials_filename)
|
| 243 |
+
and os.path.isfile(credentials_filename)
|
| 244 |
+
):
|
| 245 |
+
raise exceptions.DefaultCredentialsError(
|
| 246 |
+
"GOOGLE_APPLICATION_CREDENTIALS path is either not found or invalid."
|
| 247 |
+
)
|
| 248 |
+
|
| 249 |
+
try:
|
| 250 |
+
with open(credentials_filename, "r") as f:
|
| 251 |
+
from google.oauth2 import _service_account_async as service_account
|
| 252 |
+
|
| 253 |
+
info = json.load(f)
|
| 254 |
+
if info.get("type") == "service_account":
|
| 255 |
+
credentials = service_account.IDTokenCredentials.from_service_account_info(
|
| 256 |
+
info, target_audience=audience
|
| 257 |
+
)
|
| 258 |
+
await credentials.refresh(request)
|
| 259 |
+
return credentials.token
|
| 260 |
+
except ValueError as caught_exc:
|
| 261 |
+
new_exc = exceptions.DefaultCredentialsError(
|
| 262 |
+
"GOOGLE_APPLICATION_CREDENTIALS is not valid service account credentials.",
|
| 263 |
+
caught_exc,
|
| 264 |
+
)
|
| 265 |
+
raise new_exc from caught_exc
|
| 266 |
+
|
| 267 |
+
# 2. Try to fetch ID token from metada server if it exists. The code works
|
| 268 |
+
# for GAE and Cloud Run metadata server as well.
|
| 269 |
+
try:
|
| 270 |
+
from google.auth import compute_engine
|
| 271 |
+
from google.auth.compute_engine import _metadata
|
| 272 |
+
|
| 273 |
+
request_new = requests.Request()
|
| 274 |
+
if _metadata.ping(request_new):
|
| 275 |
+
credentials = compute_engine.IDTokenCredentials(
|
| 276 |
+
request_new, audience, use_metadata_identity_endpoint=True
|
| 277 |
+
)
|
| 278 |
+
credentials.refresh(request_new)
|
| 279 |
+
return credentials.token
|
| 280 |
+
except (ImportError, exceptions.TransportError):
|
| 281 |
+
pass
|
| 282 |
+
|
| 283 |
+
raise exceptions.DefaultCredentialsError(
|
| 284 |
+
"Neither metadata server or valid service account credentials are found."
|
| 285 |
+
)
|
.venv/lib/python3.11/site-packages/google/oauth2/_reauth_async.py
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2021 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""A module that provides functions for handling rapt authentication.
|
| 16 |
+
|
| 17 |
+
Reauth is a process of obtaining additional authentication (such as password,
|
| 18 |
+
security token, etc.) while refreshing OAuth 2.0 credentials for a user.
|
| 19 |
+
|
| 20 |
+
Credentials that use the Reauth flow must have the reauth scope,
|
| 21 |
+
``https://www.googleapis.com/auth/accounts.reauth``.
|
| 22 |
+
|
| 23 |
+
This module provides a high-level function for executing the Reauth process,
|
| 24 |
+
:func:`refresh_grant`, and lower-level helpers for doing the individual
|
| 25 |
+
steps of the reauth process.
|
| 26 |
+
|
| 27 |
+
Those steps are:
|
| 28 |
+
|
| 29 |
+
1. Obtaining a list of challenges from the reauth server.
|
| 30 |
+
2. Running through each challenge and sending the result back to the reauth
|
| 31 |
+
server.
|
| 32 |
+
3. Refreshing the access token using the returned rapt token.
|
| 33 |
+
"""
|
| 34 |
+
|
| 35 |
+
import sys
|
| 36 |
+
|
| 37 |
+
from google.auth import exceptions
|
| 38 |
+
from google.oauth2 import _client
|
| 39 |
+
from google.oauth2 import _client_async
|
| 40 |
+
from google.oauth2 import challenges
|
| 41 |
+
from google.oauth2 import reauth
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
async def _get_challenges(
|
| 45 |
+
request, supported_challenge_types, access_token, requested_scopes=None
|
| 46 |
+
):
|
| 47 |
+
"""Does initial request to reauth API to get the challenges.
|
| 48 |
+
|
| 49 |
+
Args:
|
| 50 |
+
request (google.auth.transport.Request): A callable used to make
|
| 51 |
+
HTTP requests. This must be an aiohttp request.
|
| 52 |
+
supported_challenge_types (Sequence[str]): list of challenge names
|
| 53 |
+
supported by the manager.
|
| 54 |
+
access_token (str): Access token with reauth scopes.
|
| 55 |
+
requested_scopes (Optional(Sequence[str])): Authorized scopes for the credentials.
|
| 56 |
+
|
| 57 |
+
Returns:
|
| 58 |
+
dict: The response from the reauth API.
|
| 59 |
+
"""
|
| 60 |
+
body = {"supportedChallengeTypes": supported_challenge_types}
|
| 61 |
+
if requested_scopes:
|
| 62 |
+
body["oauthScopesForDomainPolicyLookup"] = requested_scopes
|
| 63 |
+
|
| 64 |
+
return await _client_async._token_endpoint_request(
|
| 65 |
+
request,
|
| 66 |
+
reauth._REAUTH_API + ":start",
|
| 67 |
+
body,
|
| 68 |
+
access_token=access_token,
|
| 69 |
+
use_json=True,
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
async def _send_challenge_result(
|
| 74 |
+
request, session_id, challenge_id, client_input, access_token
|
| 75 |
+
):
|
| 76 |
+
"""Attempt to refresh access token by sending next challenge result.
|
| 77 |
+
|
| 78 |
+
Args:
|
| 79 |
+
request (google.auth.transport.Request): A callable used to make
|
| 80 |
+
HTTP requests. This must be an aiohttp request.
|
| 81 |
+
session_id (str): session id returned by the initial reauth call.
|
| 82 |
+
challenge_id (str): challenge id returned by the initial reauth call.
|
| 83 |
+
client_input: dict with a challenge-specific client input. For example:
|
| 84 |
+
``{'credential': password}`` for password challenge.
|
| 85 |
+
access_token (str): Access token with reauth scopes.
|
| 86 |
+
|
| 87 |
+
Returns:
|
| 88 |
+
dict: The response from the reauth API.
|
| 89 |
+
"""
|
| 90 |
+
body = {
|
| 91 |
+
"sessionId": session_id,
|
| 92 |
+
"challengeId": challenge_id,
|
| 93 |
+
"action": "RESPOND",
|
| 94 |
+
"proposalResponse": client_input,
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
return await _client_async._token_endpoint_request(
|
| 98 |
+
request,
|
| 99 |
+
reauth._REAUTH_API + "/{}:continue".format(session_id),
|
| 100 |
+
body,
|
| 101 |
+
access_token=access_token,
|
| 102 |
+
use_json=True,
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
async def _run_next_challenge(msg, request, access_token):
|
| 107 |
+
"""Get the next challenge from msg and run it.
|
| 108 |
+
|
| 109 |
+
Args:
|
| 110 |
+
msg (dict): Reauth API response body (either from the initial request to
|
| 111 |
+
https://reauth.googleapis.com/v2/sessions:start or from sending the
|
| 112 |
+
previous challenge response to
|
| 113 |
+
https://reauth.googleapis.com/v2/sessions/id:continue)
|
| 114 |
+
request (google.auth.transport.Request): A callable used to make
|
| 115 |
+
HTTP requests. This must be an aiohttp request.
|
| 116 |
+
access_token (str): reauth access token
|
| 117 |
+
|
| 118 |
+
Returns:
|
| 119 |
+
dict: The response from the reauth API.
|
| 120 |
+
|
| 121 |
+
Raises:
|
| 122 |
+
google.auth.exceptions.ReauthError: if reauth failed.
|
| 123 |
+
"""
|
| 124 |
+
for challenge in msg["challenges"]:
|
| 125 |
+
if challenge["status"] != "READY":
|
| 126 |
+
# Skip non-activated challenges.
|
| 127 |
+
continue
|
| 128 |
+
c = challenges.AVAILABLE_CHALLENGES.get(challenge["challengeType"], None)
|
| 129 |
+
if not c:
|
| 130 |
+
raise exceptions.ReauthFailError(
|
| 131 |
+
"Unsupported challenge type {0}. Supported types: {1}".format(
|
| 132 |
+
challenge["challengeType"],
|
| 133 |
+
",".join(list(challenges.AVAILABLE_CHALLENGES.keys())),
|
| 134 |
+
)
|
| 135 |
+
)
|
| 136 |
+
if not c.is_locally_eligible:
|
| 137 |
+
raise exceptions.ReauthFailError(
|
| 138 |
+
"Challenge {0} is not locally eligible".format(
|
| 139 |
+
challenge["challengeType"]
|
| 140 |
+
)
|
| 141 |
+
)
|
| 142 |
+
client_input = c.obtain_challenge_input(challenge)
|
| 143 |
+
if not client_input:
|
| 144 |
+
return None
|
| 145 |
+
return await _send_challenge_result(
|
| 146 |
+
request,
|
| 147 |
+
msg["sessionId"],
|
| 148 |
+
challenge["challengeId"],
|
| 149 |
+
client_input,
|
| 150 |
+
access_token,
|
| 151 |
+
)
|
| 152 |
+
return None
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
async def _obtain_rapt(request, access_token, requested_scopes):
|
| 156 |
+
"""Given an http request method and reauth access token, get rapt token.
|
| 157 |
+
|
| 158 |
+
Args:
|
| 159 |
+
request (google.auth.transport.Request): A callable used to make
|
| 160 |
+
HTTP requests. This must be an aiohttp request.
|
| 161 |
+
access_token (str): reauth access token
|
| 162 |
+
requested_scopes (Sequence[str]): scopes required by the client application
|
| 163 |
+
|
| 164 |
+
Returns:
|
| 165 |
+
str: The rapt token.
|
| 166 |
+
|
| 167 |
+
Raises:
|
| 168 |
+
google.auth.exceptions.ReauthError: if reauth failed
|
| 169 |
+
"""
|
| 170 |
+
msg = await _get_challenges(
|
| 171 |
+
request,
|
| 172 |
+
list(challenges.AVAILABLE_CHALLENGES.keys()),
|
| 173 |
+
access_token,
|
| 174 |
+
requested_scopes,
|
| 175 |
+
)
|
| 176 |
+
|
| 177 |
+
if msg["status"] == reauth._AUTHENTICATED:
|
| 178 |
+
return msg["encodedProofOfReauthToken"]
|
| 179 |
+
|
| 180 |
+
for _ in range(0, reauth.RUN_CHALLENGE_RETRY_LIMIT):
|
| 181 |
+
if not (
|
| 182 |
+
msg["status"] == reauth._CHALLENGE_REQUIRED
|
| 183 |
+
or msg["status"] == reauth._CHALLENGE_PENDING
|
| 184 |
+
):
|
| 185 |
+
raise exceptions.ReauthFailError(
|
| 186 |
+
"Reauthentication challenge failed due to API error: {}".format(
|
| 187 |
+
msg["status"]
|
| 188 |
+
)
|
| 189 |
+
)
|
| 190 |
+
|
| 191 |
+
if not reauth.is_interactive():
|
| 192 |
+
raise exceptions.ReauthFailError(
|
| 193 |
+
"Reauthentication challenge could not be answered because you are not"
|
| 194 |
+
" in an interactive session."
|
| 195 |
+
)
|
| 196 |
+
|
| 197 |
+
msg = await _run_next_challenge(msg, request, access_token)
|
| 198 |
+
|
| 199 |
+
if msg["status"] == reauth._AUTHENTICATED:
|
| 200 |
+
return msg["encodedProofOfReauthToken"]
|
| 201 |
+
|
| 202 |
+
# If we got here it means we didn't get authenticated.
|
| 203 |
+
raise exceptions.ReauthFailError("Failed to obtain rapt token.")
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
async def get_rapt_token(
|
| 207 |
+
request, client_id, client_secret, refresh_token, token_uri, scopes=None
|
| 208 |
+
):
|
| 209 |
+
"""Given an http request method and refresh_token, get rapt token.
|
| 210 |
+
|
| 211 |
+
Args:
|
| 212 |
+
request (google.auth.transport.Request): A callable used to make
|
| 213 |
+
HTTP requests. This must be an aiohttp request.
|
| 214 |
+
client_id (str): client id to get access token for reauth scope.
|
| 215 |
+
client_secret (str): client secret for the client_id
|
| 216 |
+
refresh_token (str): refresh token to refresh access token
|
| 217 |
+
token_uri (str): uri to refresh access token
|
| 218 |
+
scopes (Optional(Sequence[str])): scopes required by the client application
|
| 219 |
+
|
| 220 |
+
Returns:
|
| 221 |
+
str: The rapt token.
|
| 222 |
+
Raises:
|
| 223 |
+
google.auth.exceptions.RefreshError: If reauth failed.
|
| 224 |
+
"""
|
| 225 |
+
sys.stderr.write("Reauthentication required.\n")
|
| 226 |
+
|
| 227 |
+
# Get access token for reauth.
|
| 228 |
+
access_token, _, _, _ = await _client_async.refresh_grant(
|
| 229 |
+
request=request,
|
| 230 |
+
client_id=client_id,
|
| 231 |
+
client_secret=client_secret,
|
| 232 |
+
refresh_token=refresh_token,
|
| 233 |
+
token_uri=token_uri,
|
| 234 |
+
scopes=[reauth._REAUTH_SCOPE],
|
| 235 |
+
)
|
| 236 |
+
|
| 237 |
+
# Get rapt token from reauth API.
|
| 238 |
+
rapt_token = await _obtain_rapt(request, access_token, requested_scopes=scopes)
|
| 239 |
+
|
| 240 |
+
return rapt_token
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
async def refresh_grant(
|
| 244 |
+
request,
|
| 245 |
+
token_uri,
|
| 246 |
+
refresh_token,
|
| 247 |
+
client_id,
|
| 248 |
+
client_secret,
|
| 249 |
+
scopes=None,
|
| 250 |
+
rapt_token=None,
|
| 251 |
+
enable_reauth_refresh=False,
|
| 252 |
+
):
|
| 253 |
+
"""Implements the reauthentication flow.
|
| 254 |
+
|
| 255 |
+
Args:
|
| 256 |
+
request (google.auth.transport.Request): A callable used to make
|
| 257 |
+
HTTP requests. This must be an aiohttp request.
|
| 258 |
+
token_uri (str): The OAuth 2.0 authorizations server's token endpoint
|
| 259 |
+
URI.
|
| 260 |
+
refresh_token (str): The refresh token to use to get a new access
|
| 261 |
+
token.
|
| 262 |
+
client_id (str): The OAuth 2.0 application's client ID.
|
| 263 |
+
client_secret (str): The Oauth 2.0 appliaction's client secret.
|
| 264 |
+
scopes (Optional(Sequence[str])): Scopes to request. If present, all
|
| 265 |
+
scopes must be authorized for the refresh token. Useful if refresh
|
| 266 |
+
token has a wild card scope (e.g.
|
| 267 |
+
'https://www.googleapis.com/auth/any-api').
|
| 268 |
+
rapt_token (Optional(str)): The rapt token for reauth.
|
| 269 |
+
enable_reauth_refresh (Optional[bool]): Whether reauth refresh flow
|
| 270 |
+
should be used. The default value is False. This option is for
|
| 271 |
+
gcloud only, other users should use the default value.
|
| 272 |
+
|
| 273 |
+
Returns:
|
| 274 |
+
Tuple[str, Optional[str], Optional[datetime], Mapping[str, str], str]: The
|
| 275 |
+
access token, new refresh token, expiration, the additional data
|
| 276 |
+
returned by the token endpoint, and the rapt token.
|
| 277 |
+
|
| 278 |
+
Raises:
|
| 279 |
+
google.auth.exceptions.RefreshError: If the token endpoint returned
|
| 280 |
+
an error.
|
| 281 |
+
"""
|
| 282 |
+
body = {
|
| 283 |
+
"grant_type": _client._REFRESH_GRANT_TYPE,
|
| 284 |
+
"client_id": client_id,
|
| 285 |
+
"client_secret": client_secret,
|
| 286 |
+
"refresh_token": refresh_token,
|
| 287 |
+
}
|
| 288 |
+
if scopes:
|
| 289 |
+
body["scope"] = " ".join(scopes)
|
| 290 |
+
if rapt_token:
|
| 291 |
+
body["rapt"] = rapt_token
|
| 292 |
+
|
| 293 |
+
response_status_ok, response_data, retryable_error = await _client_async._token_endpoint_request_no_throw(
|
| 294 |
+
request, token_uri, body
|
| 295 |
+
)
|
| 296 |
+
if (
|
| 297 |
+
not response_status_ok
|
| 298 |
+
and response_data.get("error") == reauth._REAUTH_NEEDED_ERROR
|
| 299 |
+
and (
|
| 300 |
+
response_data.get("error_subtype")
|
| 301 |
+
== reauth._REAUTH_NEEDED_ERROR_INVALID_RAPT
|
| 302 |
+
or response_data.get("error_subtype")
|
| 303 |
+
== reauth._REAUTH_NEEDED_ERROR_RAPT_REQUIRED
|
| 304 |
+
)
|
| 305 |
+
):
|
| 306 |
+
if not enable_reauth_refresh:
|
| 307 |
+
raise exceptions.RefreshError(
|
| 308 |
+
"Reauthentication is needed. Please run `gcloud auth application-default login` to reauthenticate."
|
| 309 |
+
)
|
| 310 |
+
|
| 311 |
+
rapt_token = await get_rapt_token(
|
| 312 |
+
request, client_id, client_secret, refresh_token, token_uri, scopes=scopes
|
| 313 |
+
)
|
| 314 |
+
body["rapt"] = rapt_token
|
| 315 |
+
(
|
| 316 |
+
response_status_ok,
|
| 317 |
+
response_data,
|
| 318 |
+
retryable_error,
|
| 319 |
+
) = await _client_async._token_endpoint_request_no_throw(
|
| 320 |
+
request, token_uri, body
|
| 321 |
+
)
|
| 322 |
+
|
| 323 |
+
if not response_status_ok:
|
| 324 |
+
_client._handle_error_response(response_data, retryable_error)
|
| 325 |
+
refresh_response = _client._handle_refresh_grant_response(
|
| 326 |
+
response_data, refresh_token
|
| 327 |
+
)
|
| 328 |
+
return refresh_response + (rapt_token,)
|
.venv/lib/python3.11/site-packages/google/oauth2/_service_account_async.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2020 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""Service Accounts: JSON Web Token (JWT) Profile for OAuth 2.0
|
| 16 |
+
|
| 17 |
+
NOTE: This file adds asynchronous refresh methods to both credentials
|
| 18 |
+
classes, and therefore async/await syntax is required when calling this
|
| 19 |
+
method when using service account credentials with asynchronous functionality.
|
| 20 |
+
Otherwise, all other methods are inherited from the regular service account
|
| 21 |
+
credentials file google.oauth2.service_account
|
| 22 |
+
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
from google.auth import _credentials_async as credentials_async
|
| 26 |
+
from google.auth import _helpers
|
| 27 |
+
from google.oauth2 import _client_async
|
| 28 |
+
from google.oauth2 import service_account
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class Credentials(
|
| 32 |
+
service_account.Credentials, credentials_async.Scoped, credentials_async.Credentials
|
| 33 |
+
):
|
| 34 |
+
"""Service account credentials
|
| 35 |
+
|
| 36 |
+
Usually, you'll create these credentials with one of the helper
|
| 37 |
+
constructors. To create credentials using a Google service account
|
| 38 |
+
private key JSON file::
|
| 39 |
+
|
| 40 |
+
credentials = _service_account_async.Credentials.from_service_account_file(
|
| 41 |
+
'service-account.json')
|
| 42 |
+
|
| 43 |
+
Or if you already have the service account file loaded::
|
| 44 |
+
|
| 45 |
+
service_account_info = json.load(open('service_account.json'))
|
| 46 |
+
credentials = _service_account_async.Credentials.from_service_account_info(
|
| 47 |
+
service_account_info)
|
| 48 |
+
|
| 49 |
+
Both helper methods pass on arguments to the constructor, so you can
|
| 50 |
+
specify additional scopes and a subject if necessary::
|
| 51 |
+
|
| 52 |
+
credentials = _service_account_async.Credentials.from_service_account_file(
|
| 53 |
+
'service-account.json',
|
| 54 |
+
scopes=['email'],
|
| 55 |
+
subject='user@example.com')
|
| 56 |
+
|
| 57 |
+
The credentials are considered immutable. If you want to modify the scopes
|
| 58 |
+
or the subject used for delegation, use :meth:`with_scopes` or
|
| 59 |
+
:meth:`with_subject`::
|
| 60 |
+
|
| 61 |
+
scoped_credentials = credentials.with_scopes(['email'])
|
| 62 |
+
delegated_credentials = credentials.with_subject(subject)
|
| 63 |
+
|
| 64 |
+
To add a quota project, use :meth:`with_quota_project`::
|
| 65 |
+
|
| 66 |
+
credentials = credentials.with_quota_project('myproject-123')
|
| 67 |
+
"""
|
| 68 |
+
|
| 69 |
+
@_helpers.copy_docstring(credentials_async.Credentials)
|
| 70 |
+
async def refresh(self, request):
|
| 71 |
+
assertion = self._make_authorization_grant_assertion()
|
| 72 |
+
access_token, expiry, _ = await _client_async.jwt_grant(
|
| 73 |
+
request, self._token_uri, assertion
|
| 74 |
+
)
|
| 75 |
+
self.token = access_token
|
| 76 |
+
self.expiry = expiry
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
class IDTokenCredentials(
|
| 80 |
+
service_account.IDTokenCredentials,
|
| 81 |
+
credentials_async.Signing,
|
| 82 |
+
credentials_async.Credentials,
|
| 83 |
+
):
|
| 84 |
+
"""Open ID Connect ID Token-based service account credentials.
|
| 85 |
+
|
| 86 |
+
These credentials are largely similar to :class:`.Credentials`, but instead
|
| 87 |
+
of using an OAuth 2.0 Access Token as the bearer token, they use an Open
|
| 88 |
+
ID Connect ID Token as the bearer token. These credentials are useful when
|
| 89 |
+
communicating to services that require ID Tokens and can not accept access
|
| 90 |
+
tokens.
|
| 91 |
+
|
| 92 |
+
Usually, you'll create these credentials with one of the helper
|
| 93 |
+
constructors. To create credentials using a Google service account
|
| 94 |
+
private key JSON file::
|
| 95 |
+
|
| 96 |
+
credentials = (
|
| 97 |
+
_service_account_async.IDTokenCredentials.from_service_account_file(
|
| 98 |
+
'service-account.json'))
|
| 99 |
+
|
| 100 |
+
Or if you already have the service account file loaded::
|
| 101 |
+
|
| 102 |
+
service_account_info = json.load(open('service_account.json'))
|
| 103 |
+
credentials = (
|
| 104 |
+
_service_account_async.IDTokenCredentials.from_service_account_info(
|
| 105 |
+
service_account_info))
|
| 106 |
+
|
| 107 |
+
Both helper methods pass on arguments to the constructor, so you can
|
| 108 |
+
specify additional scopes and a subject if necessary::
|
| 109 |
+
|
| 110 |
+
credentials = (
|
| 111 |
+
_service_account_async.IDTokenCredentials.from_service_account_file(
|
| 112 |
+
'service-account.json',
|
| 113 |
+
scopes=['email'],
|
| 114 |
+
subject='user@example.com'))
|
| 115 |
+
|
| 116 |
+
The credentials are considered immutable. If you want to modify the scopes
|
| 117 |
+
or the subject used for delegation, use :meth:`with_scopes` or
|
| 118 |
+
:meth:`with_subject`::
|
| 119 |
+
|
| 120 |
+
scoped_credentials = credentials.with_scopes(['email'])
|
| 121 |
+
delegated_credentials = credentials.with_subject(subject)
|
| 122 |
+
|
| 123 |
+
"""
|
| 124 |
+
|
| 125 |
+
@_helpers.copy_docstring(credentials_async.Credentials)
|
| 126 |
+
async def refresh(self, request):
|
| 127 |
+
assertion = self._make_authorization_grant_assertion()
|
| 128 |
+
access_token, expiry, _ = await _client_async.id_token_jwt_grant(
|
| 129 |
+
request, self._token_uri, assertion
|
| 130 |
+
)
|
| 131 |
+
self.token = access_token
|
| 132 |
+
self.expiry = expiry
|
.venv/lib/python3.11/site-packages/google/oauth2/challenges.py
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2021 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
""" Challenges for reauthentication.
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
import abc
|
| 19 |
+
import base64
|
| 20 |
+
import getpass
|
| 21 |
+
import sys
|
| 22 |
+
|
| 23 |
+
from google.auth import _helpers
|
| 24 |
+
from google.auth import exceptions
|
| 25 |
+
from google.oauth2 import webauthn_handler_factory
|
| 26 |
+
from google.oauth2.webauthn_types import (
|
| 27 |
+
AuthenticationExtensionsClientInputs,
|
| 28 |
+
GetRequest,
|
| 29 |
+
PublicKeyCredentialDescriptor,
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
REAUTH_ORIGIN = "https://accounts.google.com"
|
| 34 |
+
SAML_CHALLENGE_MESSAGE = (
|
| 35 |
+
"Please run `gcloud auth login` to complete reauthentication with SAML."
|
| 36 |
+
)
|
| 37 |
+
WEBAUTHN_TIMEOUT_MS = 120000 # Two minute timeout
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def get_user_password(text):
|
| 41 |
+
"""Get password from user.
|
| 42 |
+
|
| 43 |
+
Override this function with a different logic if you are using this library
|
| 44 |
+
outside a CLI.
|
| 45 |
+
|
| 46 |
+
Args:
|
| 47 |
+
text (str): message for the password prompt.
|
| 48 |
+
|
| 49 |
+
Returns:
|
| 50 |
+
str: password string.
|
| 51 |
+
"""
|
| 52 |
+
return getpass.getpass(text)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
class ReauthChallenge(metaclass=abc.ABCMeta):
|
| 56 |
+
"""Base class for reauth challenges."""
|
| 57 |
+
|
| 58 |
+
@property
|
| 59 |
+
@abc.abstractmethod
|
| 60 |
+
def name(self): # pragma: NO COVER
|
| 61 |
+
"""Returns the name of the challenge."""
|
| 62 |
+
raise NotImplementedError("name property must be implemented")
|
| 63 |
+
|
| 64 |
+
@property
|
| 65 |
+
@abc.abstractmethod
|
| 66 |
+
def is_locally_eligible(self): # pragma: NO COVER
|
| 67 |
+
"""Returns true if a challenge is supported locally on this machine."""
|
| 68 |
+
raise NotImplementedError("is_locally_eligible property must be implemented")
|
| 69 |
+
|
| 70 |
+
@abc.abstractmethod
|
| 71 |
+
def obtain_challenge_input(self, metadata): # pragma: NO COVER
|
| 72 |
+
"""Performs logic required to obtain credentials and returns it.
|
| 73 |
+
|
| 74 |
+
Args:
|
| 75 |
+
metadata (Mapping): challenge metadata returned in the 'challenges' field in
|
| 76 |
+
the initial reauth request. Includes the 'challengeType' field
|
| 77 |
+
and other challenge-specific fields.
|
| 78 |
+
|
| 79 |
+
Returns:
|
| 80 |
+
response that will be send to the reauth service as the content of
|
| 81 |
+
the 'proposalResponse' field in the request body. Usually a dict
|
| 82 |
+
with the keys specific to the challenge. For example,
|
| 83 |
+
``{'credential': password}`` for password challenge.
|
| 84 |
+
"""
|
| 85 |
+
raise NotImplementedError("obtain_challenge_input method must be implemented")
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
class PasswordChallenge(ReauthChallenge):
|
| 89 |
+
"""Challenge that asks for user's password."""
|
| 90 |
+
|
| 91 |
+
@property
|
| 92 |
+
def name(self):
|
| 93 |
+
return "PASSWORD"
|
| 94 |
+
|
| 95 |
+
@property
|
| 96 |
+
def is_locally_eligible(self):
|
| 97 |
+
return True
|
| 98 |
+
|
| 99 |
+
@_helpers.copy_docstring(ReauthChallenge)
|
| 100 |
+
def obtain_challenge_input(self, unused_metadata):
|
| 101 |
+
passwd = get_user_password("Please enter your password:")
|
| 102 |
+
if not passwd:
|
| 103 |
+
passwd = " " # avoid the server crashing in case of no password :D
|
| 104 |
+
return {"credential": passwd}
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
class SecurityKeyChallenge(ReauthChallenge):
|
| 108 |
+
"""Challenge that asks for user's security key touch."""
|
| 109 |
+
|
| 110 |
+
@property
|
| 111 |
+
def name(self):
|
| 112 |
+
return "SECURITY_KEY"
|
| 113 |
+
|
| 114 |
+
@property
|
| 115 |
+
def is_locally_eligible(self):
|
| 116 |
+
return True
|
| 117 |
+
|
| 118 |
+
@_helpers.copy_docstring(ReauthChallenge)
|
| 119 |
+
def obtain_challenge_input(self, metadata):
|
| 120 |
+
# Check if there is an available Webauthn Handler, if not use pyu2f
|
| 121 |
+
try:
|
| 122 |
+
factory = webauthn_handler_factory.WebauthnHandlerFactory()
|
| 123 |
+
webauthn_handler = factory.get_handler()
|
| 124 |
+
if webauthn_handler is not None:
|
| 125 |
+
sys.stderr.write("Please insert and touch your security key\n")
|
| 126 |
+
return self._obtain_challenge_input_webauthn(metadata, webauthn_handler)
|
| 127 |
+
except Exception:
|
| 128 |
+
# Attempt pyu2f if exception in webauthn flow
|
| 129 |
+
pass
|
| 130 |
+
|
| 131 |
+
try:
|
| 132 |
+
import pyu2f.convenience.authenticator # type: ignore
|
| 133 |
+
import pyu2f.errors # type: ignore
|
| 134 |
+
import pyu2f.model # type: ignore
|
| 135 |
+
except ImportError:
|
| 136 |
+
raise exceptions.ReauthFailError(
|
| 137 |
+
"pyu2f dependency is required to use Security key reauth feature. "
|
| 138 |
+
"It can be installed via `pip install pyu2f` or `pip install google-auth[reauth]`."
|
| 139 |
+
)
|
| 140 |
+
sk = metadata["securityKey"]
|
| 141 |
+
challenges = sk["challenges"]
|
| 142 |
+
# Read both 'applicationId' and 'relyingPartyId', if they are the same, use
|
| 143 |
+
# applicationId, if they are different, use relyingPartyId first and retry
|
| 144 |
+
# with applicationId
|
| 145 |
+
application_id = sk["applicationId"]
|
| 146 |
+
relying_party_id = sk["relyingPartyId"]
|
| 147 |
+
|
| 148 |
+
if application_id != relying_party_id:
|
| 149 |
+
application_parameters = [relying_party_id, application_id]
|
| 150 |
+
else:
|
| 151 |
+
application_parameters = [application_id]
|
| 152 |
+
|
| 153 |
+
challenge_data = []
|
| 154 |
+
for c in challenges:
|
| 155 |
+
kh = c["keyHandle"].encode("ascii")
|
| 156 |
+
key = pyu2f.model.RegisteredKey(bytearray(base64.urlsafe_b64decode(kh)))
|
| 157 |
+
challenge = c["challenge"].encode("ascii")
|
| 158 |
+
challenge = base64.urlsafe_b64decode(challenge)
|
| 159 |
+
challenge_data.append({"key": key, "challenge": challenge})
|
| 160 |
+
|
| 161 |
+
# Track number of tries to suppress error message until all application_parameters
|
| 162 |
+
# are tried.
|
| 163 |
+
tries = 0
|
| 164 |
+
for app_id in application_parameters:
|
| 165 |
+
try:
|
| 166 |
+
tries += 1
|
| 167 |
+
api = pyu2f.convenience.authenticator.CreateCompositeAuthenticator(
|
| 168 |
+
REAUTH_ORIGIN
|
| 169 |
+
)
|
| 170 |
+
response = api.Authenticate(
|
| 171 |
+
app_id, challenge_data, print_callback=sys.stderr.write
|
| 172 |
+
)
|
| 173 |
+
return {"securityKey": response}
|
| 174 |
+
except pyu2f.errors.U2FError as e:
|
| 175 |
+
if e.code == pyu2f.errors.U2FError.DEVICE_INELIGIBLE:
|
| 176 |
+
# Only show error if all app_ids have been tried
|
| 177 |
+
if tries == len(application_parameters):
|
| 178 |
+
sys.stderr.write("Ineligible security key.\n")
|
| 179 |
+
return None
|
| 180 |
+
continue
|
| 181 |
+
if e.code == pyu2f.errors.U2FError.TIMEOUT:
|
| 182 |
+
sys.stderr.write(
|
| 183 |
+
"Timed out while waiting for security key touch.\n"
|
| 184 |
+
)
|
| 185 |
+
else:
|
| 186 |
+
raise e
|
| 187 |
+
except pyu2f.errors.PluginError as e:
|
| 188 |
+
sys.stderr.write("Plugin error: {}.\n".format(e))
|
| 189 |
+
continue
|
| 190 |
+
except pyu2f.errors.NoDeviceFoundError:
|
| 191 |
+
sys.stderr.write("No security key found.\n")
|
| 192 |
+
return None
|
| 193 |
+
|
| 194 |
+
def _obtain_challenge_input_webauthn(self, metadata, webauthn_handler):
|
| 195 |
+
sk = metadata.get("securityKey")
|
| 196 |
+
if sk is None:
|
| 197 |
+
raise exceptions.InvalidValue("securityKey is None")
|
| 198 |
+
challenges = sk.get("challenges")
|
| 199 |
+
application_id = sk.get("applicationId")
|
| 200 |
+
relying_party_id = sk.get("relyingPartyId")
|
| 201 |
+
if challenges is None or len(challenges) < 1:
|
| 202 |
+
raise exceptions.InvalidValue("challenges is None or empty")
|
| 203 |
+
if application_id is None:
|
| 204 |
+
raise exceptions.InvalidValue("application_id is None")
|
| 205 |
+
if relying_party_id is None:
|
| 206 |
+
raise exceptions.InvalidValue("relying_party_id is None")
|
| 207 |
+
|
| 208 |
+
allow_credentials = []
|
| 209 |
+
for challenge in challenges:
|
| 210 |
+
kh = challenge.get("keyHandle")
|
| 211 |
+
if kh is None:
|
| 212 |
+
raise exceptions.InvalidValue("keyHandle is None")
|
| 213 |
+
key_handle = self._unpadded_urlsafe_b64recode(kh)
|
| 214 |
+
allow_credentials.append(PublicKeyCredentialDescriptor(id=key_handle))
|
| 215 |
+
|
| 216 |
+
extension = AuthenticationExtensionsClientInputs(appid=application_id)
|
| 217 |
+
|
| 218 |
+
challenge = challenges[0].get("challenge")
|
| 219 |
+
if challenge is None:
|
| 220 |
+
raise exceptions.InvalidValue("challenge is None")
|
| 221 |
+
|
| 222 |
+
get_request = GetRequest(
|
| 223 |
+
origin=REAUTH_ORIGIN,
|
| 224 |
+
rpid=relying_party_id,
|
| 225 |
+
challenge=self._unpadded_urlsafe_b64recode(challenge),
|
| 226 |
+
timeout_ms=WEBAUTHN_TIMEOUT_MS,
|
| 227 |
+
allow_credentials=allow_credentials,
|
| 228 |
+
user_verification="required",
|
| 229 |
+
extensions=extension,
|
| 230 |
+
)
|
| 231 |
+
|
| 232 |
+
try:
|
| 233 |
+
get_response = webauthn_handler.get(get_request)
|
| 234 |
+
except Exception as e:
|
| 235 |
+
sys.stderr.write("Webauthn Error: {}.\n".format(e))
|
| 236 |
+
raise e
|
| 237 |
+
|
| 238 |
+
response = {
|
| 239 |
+
"clientData": get_response.response.client_data_json,
|
| 240 |
+
"authenticatorData": get_response.response.authenticator_data,
|
| 241 |
+
"signatureData": get_response.response.signature,
|
| 242 |
+
"applicationId": application_id,
|
| 243 |
+
"keyHandle": get_response.id,
|
| 244 |
+
"securityKeyReplyType": 2,
|
| 245 |
+
}
|
| 246 |
+
return {"securityKey": response}
|
| 247 |
+
|
| 248 |
+
def _unpadded_urlsafe_b64recode(self, s):
|
| 249 |
+
"""Converts standard b64 encoded string to url safe b64 encoded string
|
| 250 |
+
with no padding."""
|
| 251 |
+
b = base64.urlsafe_b64decode(s)
|
| 252 |
+
return base64.urlsafe_b64encode(b).decode().rstrip("=")
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
class SamlChallenge(ReauthChallenge):
|
| 256 |
+
"""Challenge that asks the users to browse to their ID Providers.
|
| 257 |
+
|
| 258 |
+
Currently SAML challenge is not supported. When obtaining the challenge
|
| 259 |
+
input, exception will be raised to instruct the users to run
|
| 260 |
+
`gcloud auth login` for reauthentication.
|
| 261 |
+
"""
|
| 262 |
+
|
| 263 |
+
@property
|
| 264 |
+
def name(self):
|
| 265 |
+
return "SAML"
|
| 266 |
+
|
| 267 |
+
@property
|
| 268 |
+
def is_locally_eligible(self):
|
| 269 |
+
return True
|
| 270 |
+
|
| 271 |
+
def obtain_challenge_input(self, metadata):
|
| 272 |
+
# Magic Arch has not fully supported returning a proper dedirect URL
|
| 273 |
+
# for programmatic SAML users today. So we error our here and request
|
| 274 |
+
# users to use gcloud to complete a login.
|
| 275 |
+
raise exceptions.ReauthSamlChallengeFailError(SAML_CHALLENGE_MESSAGE)
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
AVAILABLE_CHALLENGES = {
|
| 279 |
+
challenge.name: challenge
|
| 280 |
+
for challenge in [SecurityKeyChallenge(), PasswordChallenge(), SamlChallenge()]
|
| 281 |
+
}
|
.venv/lib/python3.11/site-packages/google/oauth2/credentials.py
ADDED
|
@@ -0,0 +1,614 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2016 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""OAuth 2.0 Credentials.
|
| 16 |
+
|
| 17 |
+
This module provides credentials based on OAuth 2.0 access and refresh tokens.
|
| 18 |
+
These credentials usually access resources on behalf of a user (resource
|
| 19 |
+
owner).
|
| 20 |
+
|
| 21 |
+
Specifically, this is intended to use access tokens acquired using the
|
| 22 |
+
`Authorization Code grant`_ and can refresh those tokens using a
|
| 23 |
+
optional `refresh token`_.
|
| 24 |
+
|
| 25 |
+
Obtaining the initial access and refresh token is outside of the scope of this
|
| 26 |
+
module. Consult `rfc6749 section 4.1`_ for complete details on the
|
| 27 |
+
Authorization Code grant flow.
|
| 28 |
+
|
| 29 |
+
.. _Authorization Code grant: https://tools.ietf.org/html/rfc6749#section-1.3.1
|
| 30 |
+
.. _refresh token: https://tools.ietf.org/html/rfc6749#section-6
|
| 31 |
+
.. _rfc6749 section 4.1: https://tools.ietf.org/html/rfc6749#section-4.1
|
| 32 |
+
"""
|
| 33 |
+
|
| 34 |
+
from datetime import datetime
|
| 35 |
+
import io
|
| 36 |
+
import json
|
| 37 |
+
import logging
|
| 38 |
+
import warnings
|
| 39 |
+
|
| 40 |
+
from google.auth import _cloud_sdk
|
| 41 |
+
from google.auth import _helpers
|
| 42 |
+
from google.auth import credentials
|
| 43 |
+
from google.auth import exceptions
|
| 44 |
+
from google.auth import metrics
|
| 45 |
+
from google.oauth2 import reauth
|
| 46 |
+
|
| 47 |
+
_LOGGER = logging.getLogger(__name__)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
# The Google OAuth 2.0 token endpoint. Used for authorized user credentials.
|
| 51 |
+
_GOOGLE_OAUTH2_TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token"
|
| 52 |
+
|
| 53 |
+
# The Google OAuth 2.0 token info endpoint. Used for getting token info JSON from access tokens.
|
| 54 |
+
_GOOGLE_OAUTH2_TOKEN_INFO_ENDPOINT = "https://oauth2.googleapis.com/tokeninfo"
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
class Credentials(credentials.ReadOnlyScoped, credentials.CredentialsWithQuotaProject):
|
| 58 |
+
"""Credentials using OAuth 2.0 access and refresh tokens.
|
| 59 |
+
|
| 60 |
+
The credentials are considered immutable except the tokens and the token
|
| 61 |
+
expiry, which are updated after refresh. If you want to modify the quota
|
| 62 |
+
project, use :meth:`with_quota_project` or ::
|
| 63 |
+
|
| 64 |
+
credentials = credentials.with_quota_project('myproject-123')
|
| 65 |
+
|
| 66 |
+
Reauth is disabled by default. To enable reauth, set the
|
| 67 |
+
`enable_reauth_refresh` parameter to True in the constructor. Note that
|
| 68 |
+
reauth feature is intended for gcloud to use only.
|
| 69 |
+
If reauth is enabled, `pyu2f` dependency has to be installed in order to use security
|
| 70 |
+
key reauth feature. Dependency can be installed via `pip install pyu2f` or `pip install
|
| 71 |
+
google-auth[reauth]`.
|
| 72 |
+
"""
|
| 73 |
+
|
| 74 |
+
def __init__(
|
| 75 |
+
self,
|
| 76 |
+
token,
|
| 77 |
+
refresh_token=None,
|
| 78 |
+
id_token=None,
|
| 79 |
+
token_uri=None,
|
| 80 |
+
client_id=None,
|
| 81 |
+
client_secret=None,
|
| 82 |
+
scopes=None,
|
| 83 |
+
default_scopes=None,
|
| 84 |
+
quota_project_id=None,
|
| 85 |
+
expiry=None,
|
| 86 |
+
rapt_token=None,
|
| 87 |
+
refresh_handler=None,
|
| 88 |
+
enable_reauth_refresh=False,
|
| 89 |
+
granted_scopes=None,
|
| 90 |
+
trust_boundary=None,
|
| 91 |
+
universe_domain=credentials.DEFAULT_UNIVERSE_DOMAIN,
|
| 92 |
+
account=None,
|
| 93 |
+
):
|
| 94 |
+
"""
|
| 95 |
+
Args:
|
| 96 |
+
token (Optional(str)): The OAuth 2.0 access token. Can be None
|
| 97 |
+
if refresh information is provided.
|
| 98 |
+
refresh_token (str): The OAuth 2.0 refresh token. If specified,
|
| 99 |
+
credentials can be refreshed.
|
| 100 |
+
id_token (str): The Open ID Connect ID Token.
|
| 101 |
+
token_uri (str): The OAuth 2.0 authorization server's token
|
| 102 |
+
endpoint URI. Must be specified for refresh, can be left as
|
| 103 |
+
None if the token can not be refreshed.
|
| 104 |
+
client_id (str): The OAuth 2.0 client ID. Must be specified for
|
| 105 |
+
refresh, can be left as None if the token can not be refreshed.
|
| 106 |
+
client_secret(str): The OAuth 2.0 client secret. Must be specified
|
| 107 |
+
for refresh, can be left as None if the token can not be
|
| 108 |
+
refreshed.
|
| 109 |
+
scopes (Sequence[str]): The scopes used to obtain authorization.
|
| 110 |
+
This parameter is used by :meth:`has_scopes`. OAuth 2.0
|
| 111 |
+
credentials can not request additional scopes after
|
| 112 |
+
authorization. The scopes must be derivable from the refresh
|
| 113 |
+
token if refresh information is provided (e.g. The refresh
|
| 114 |
+
token scopes are a superset of this or contain a wild card
|
| 115 |
+
scope like 'https://www.googleapis.com/auth/any-api').
|
| 116 |
+
default_scopes (Sequence[str]): Default scopes passed by a
|
| 117 |
+
Google client library. Use 'scopes' for user-defined scopes.
|
| 118 |
+
quota_project_id (Optional[str]): The project ID used for quota and billing.
|
| 119 |
+
This project may be different from the project used to
|
| 120 |
+
create the credentials.
|
| 121 |
+
rapt_token (Optional[str]): The reauth Proof Token.
|
| 122 |
+
refresh_handler (Optional[Callable[[google.auth.transport.Request, Sequence[str]], [str, datetime]]]):
|
| 123 |
+
A callable which takes in the HTTP request callable and the list of
|
| 124 |
+
OAuth scopes and when called returns an access token string for the
|
| 125 |
+
requested scopes and its expiry datetime. This is useful when no
|
| 126 |
+
refresh tokens are provided and tokens are obtained by calling
|
| 127 |
+
some external process on demand. It is particularly useful for
|
| 128 |
+
retrieving downscoped tokens from a token broker.
|
| 129 |
+
enable_reauth_refresh (Optional[bool]): Whether reauth refresh flow
|
| 130 |
+
should be used. This flag is for gcloud to use only.
|
| 131 |
+
granted_scopes (Optional[Sequence[str]]): The scopes that were consented/granted by the user.
|
| 132 |
+
This could be different from the requested scopes and it could be empty if granted
|
| 133 |
+
and requested scopes were same.
|
| 134 |
+
trust_boundary (str): String representation of trust boundary meta.
|
| 135 |
+
universe_domain (Optional[str]): The universe domain. The default
|
| 136 |
+
universe domain is googleapis.com.
|
| 137 |
+
account (Optional[str]): The account associated with the credential.
|
| 138 |
+
"""
|
| 139 |
+
super(Credentials, self).__init__()
|
| 140 |
+
self.token = token
|
| 141 |
+
self.expiry = expiry
|
| 142 |
+
self._refresh_token = refresh_token
|
| 143 |
+
self._id_token = id_token
|
| 144 |
+
self._scopes = scopes
|
| 145 |
+
self._default_scopes = default_scopes
|
| 146 |
+
self._granted_scopes = granted_scopes
|
| 147 |
+
self._token_uri = token_uri
|
| 148 |
+
self._client_id = client_id
|
| 149 |
+
self._client_secret = client_secret
|
| 150 |
+
self._quota_project_id = quota_project_id
|
| 151 |
+
self._rapt_token = rapt_token
|
| 152 |
+
self.refresh_handler = refresh_handler
|
| 153 |
+
self._enable_reauth_refresh = enable_reauth_refresh
|
| 154 |
+
self._trust_boundary = trust_boundary
|
| 155 |
+
self._universe_domain = universe_domain or credentials.DEFAULT_UNIVERSE_DOMAIN
|
| 156 |
+
self._account = account or ""
|
| 157 |
+
self._cred_file_path = None
|
| 158 |
+
|
| 159 |
+
def __getstate__(self):
|
| 160 |
+
"""A __getstate__ method must exist for the __setstate__ to be called
|
| 161 |
+
This is identical to the default implementation.
|
| 162 |
+
See https://docs.python.org/3.7/library/pickle.html#object.__setstate__
|
| 163 |
+
"""
|
| 164 |
+
state_dict = self.__dict__.copy()
|
| 165 |
+
# Remove _refresh_handler function as there are limitations pickling and
|
| 166 |
+
# unpickling certain callables (lambda, functools.partial instances)
|
| 167 |
+
# because they need to be importable.
|
| 168 |
+
# Instead, the refresh_handler setter should be used to repopulate this.
|
| 169 |
+
if "_refresh_handler" in state_dict:
|
| 170 |
+
del state_dict["_refresh_handler"]
|
| 171 |
+
|
| 172 |
+
if "_refresh_worker" in state_dict:
|
| 173 |
+
del state_dict["_refresh_worker"]
|
| 174 |
+
return state_dict
|
| 175 |
+
|
| 176 |
+
def __setstate__(self, d):
|
| 177 |
+
"""Credentials pickled with older versions of the class do not have
|
| 178 |
+
all the attributes."""
|
| 179 |
+
self.token = d.get("token")
|
| 180 |
+
self.expiry = d.get("expiry")
|
| 181 |
+
self._refresh_token = d.get("_refresh_token")
|
| 182 |
+
self._id_token = d.get("_id_token")
|
| 183 |
+
self._scopes = d.get("_scopes")
|
| 184 |
+
self._default_scopes = d.get("_default_scopes")
|
| 185 |
+
self._granted_scopes = d.get("_granted_scopes")
|
| 186 |
+
self._token_uri = d.get("_token_uri")
|
| 187 |
+
self._client_id = d.get("_client_id")
|
| 188 |
+
self._client_secret = d.get("_client_secret")
|
| 189 |
+
self._quota_project_id = d.get("_quota_project_id")
|
| 190 |
+
self._rapt_token = d.get("_rapt_token")
|
| 191 |
+
self._enable_reauth_refresh = d.get("_enable_reauth_refresh")
|
| 192 |
+
self._trust_boundary = d.get("_trust_boundary")
|
| 193 |
+
self._universe_domain = (
|
| 194 |
+
d.get("_universe_domain") or credentials.DEFAULT_UNIVERSE_DOMAIN
|
| 195 |
+
)
|
| 196 |
+
self._cred_file_path = d.get("_cred_file_path")
|
| 197 |
+
# The refresh_handler setter should be used to repopulate this.
|
| 198 |
+
self._refresh_handler = None
|
| 199 |
+
self._refresh_worker = None
|
| 200 |
+
self._use_non_blocking_refresh = d.get("_use_non_blocking_refresh", False)
|
| 201 |
+
self._account = d.get("_account", "")
|
| 202 |
+
|
| 203 |
+
@property
|
| 204 |
+
def refresh_token(self):
|
| 205 |
+
"""Optional[str]: The OAuth 2.0 refresh token."""
|
| 206 |
+
return self._refresh_token
|
| 207 |
+
|
| 208 |
+
@property
|
| 209 |
+
def scopes(self):
|
| 210 |
+
"""Optional[str]: The OAuth 2.0 permission scopes."""
|
| 211 |
+
return self._scopes
|
| 212 |
+
|
| 213 |
+
@property
|
| 214 |
+
def granted_scopes(self):
|
| 215 |
+
"""Optional[Sequence[str]]: The OAuth 2.0 permission scopes that were granted by the user."""
|
| 216 |
+
return self._granted_scopes
|
| 217 |
+
|
| 218 |
+
@property
|
| 219 |
+
def token_uri(self):
|
| 220 |
+
"""Optional[str]: The OAuth 2.0 authorization server's token endpoint
|
| 221 |
+
URI."""
|
| 222 |
+
return self._token_uri
|
| 223 |
+
|
| 224 |
+
@property
|
| 225 |
+
def id_token(self):
|
| 226 |
+
"""Optional[str]: The Open ID Connect ID Token.
|
| 227 |
+
|
| 228 |
+
Depending on the authorization server and the scopes requested, this
|
| 229 |
+
may be populated when credentials are obtained and updated when
|
| 230 |
+
:meth:`refresh` is called. This token is a JWT. It can be verified
|
| 231 |
+
and decoded using :func:`google.oauth2.id_token.verify_oauth2_token`.
|
| 232 |
+
"""
|
| 233 |
+
return self._id_token
|
| 234 |
+
|
| 235 |
+
@property
|
| 236 |
+
def client_id(self):
|
| 237 |
+
"""Optional[str]: The OAuth 2.0 client ID."""
|
| 238 |
+
return self._client_id
|
| 239 |
+
|
| 240 |
+
@property
|
| 241 |
+
def client_secret(self):
|
| 242 |
+
"""Optional[str]: The OAuth 2.0 client secret."""
|
| 243 |
+
return self._client_secret
|
| 244 |
+
|
| 245 |
+
@property
|
| 246 |
+
def requires_scopes(self):
|
| 247 |
+
"""False: OAuth 2.0 credentials have their scopes set when
|
| 248 |
+
the initial token is requested and can not be changed."""
|
| 249 |
+
return False
|
| 250 |
+
|
| 251 |
+
@property
|
| 252 |
+
def rapt_token(self):
|
| 253 |
+
"""Optional[str]: The reauth Proof Token."""
|
| 254 |
+
return self._rapt_token
|
| 255 |
+
|
| 256 |
+
@property
|
| 257 |
+
def refresh_handler(self):
|
| 258 |
+
"""Returns the refresh handler if available.
|
| 259 |
+
|
| 260 |
+
Returns:
|
| 261 |
+
Optional[Callable[[google.auth.transport.Request, Sequence[str]], [str, datetime]]]:
|
| 262 |
+
The current refresh handler.
|
| 263 |
+
"""
|
| 264 |
+
return self._refresh_handler
|
| 265 |
+
|
| 266 |
+
@refresh_handler.setter
|
| 267 |
+
def refresh_handler(self, value):
|
| 268 |
+
"""Updates the current refresh handler.
|
| 269 |
+
|
| 270 |
+
Args:
|
| 271 |
+
value (Optional[Callable[[google.auth.transport.Request, Sequence[str]], [str, datetime]]]):
|
| 272 |
+
The updated value of the refresh handler.
|
| 273 |
+
|
| 274 |
+
Raises:
|
| 275 |
+
TypeError: If the value is not a callable or None.
|
| 276 |
+
"""
|
| 277 |
+
if not callable(value) and value is not None:
|
| 278 |
+
raise TypeError("The provided refresh_handler is not a callable or None.")
|
| 279 |
+
self._refresh_handler = value
|
| 280 |
+
|
| 281 |
+
@property
|
| 282 |
+
def account(self):
|
| 283 |
+
"""str: The user account associated with the credential. If the account is unknown an empty string is returned."""
|
| 284 |
+
return self._account
|
| 285 |
+
|
| 286 |
+
def _make_copy(self):
|
| 287 |
+
cred = self.__class__(
|
| 288 |
+
self.token,
|
| 289 |
+
refresh_token=self.refresh_token,
|
| 290 |
+
id_token=self.id_token,
|
| 291 |
+
token_uri=self.token_uri,
|
| 292 |
+
client_id=self.client_id,
|
| 293 |
+
client_secret=self.client_secret,
|
| 294 |
+
scopes=self.scopes,
|
| 295 |
+
default_scopes=self.default_scopes,
|
| 296 |
+
granted_scopes=self.granted_scopes,
|
| 297 |
+
quota_project_id=self.quota_project_id,
|
| 298 |
+
rapt_token=self.rapt_token,
|
| 299 |
+
enable_reauth_refresh=self._enable_reauth_refresh,
|
| 300 |
+
trust_boundary=self._trust_boundary,
|
| 301 |
+
universe_domain=self._universe_domain,
|
| 302 |
+
account=self._account,
|
| 303 |
+
)
|
| 304 |
+
cred._cred_file_path = self._cred_file_path
|
| 305 |
+
return cred
|
| 306 |
+
|
| 307 |
+
@_helpers.copy_docstring(credentials.Credentials)
|
| 308 |
+
def get_cred_info(self):
|
| 309 |
+
if self._cred_file_path:
|
| 310 |
+
cred_info = {
|
| 311 |
+
"credential_source": self._cred_file_path,
|
| 312 |
+
"credential_type": "user credentials",
|
| 313 |
+
}
|
| 314 |
+
if self.account:
|
| 315 |
+
cred_info["principal"] = self.account
|
| 316 |
+
return cred_info
|
| 317 |
+
return None
|
| 318 |
+
|
| 319 |
+
@_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
|
| 320 |
+
def with_quota_project(self, quota_project_id):
|
| 321 |
+
cred = self._make_copy()
|
| 322 |
+
cred._quota_project_id = quota_project_id
|
| 323 |
+
return cred
|
| 324 |
+
|
| 325 |
+
@_helpers.copy_docstring(credentials.CredentialsWithTokenUri)
|
| 326 |
+
def with_token_uri(self, token_uri):
|
| 327 |
+
cred = self._make_copy()
|
| 328 |
+
cred._token_uri = token_uri
|
| 329 |
+
return cred
|
| 330 |
+
|
| 331 |
+
def with_account(self, account):
|
| 332 |
+
"""Returns a copy of these credentials with a modified account.
|
| 333 |
+
|
| 334 |
+
Args:
|
| 335 |
+
account (str): The account to set
|
| 336 |
+
|
| 337 |
+
Returns:
|
| 338 |
+
google.oauth2.credentials.Credentials: A new credentials instance.
|
| 339 |
+
"""
|
| 340 |
+
cred = self._make_copy()
|
| 341 |
+
cred._account = account
|
| 342 |
+
return cred
|
| 343 |
+
|
| 344 |
+
@_helpers.copy_docstring(credentials.CredentialsWithUniverseDomain)
|
| 345 |
+
def with_universe_domain(self, universe_domain):
|
| 346 |
+
cred = self._make_copy()
|
| 347 |
+
cred._universe_domain = universe_domain
|
| 348 |
+
return cred
|
| 349 |
+
|
| 350 |
+
def _metric_header_for_usage(self):
|
| 351 |
+
return metrics.CRED_TYPE_USER
|
| 352 |
+
|
| 353 |
+
@_helpers.copy_docstring(credentials.Credentials)
|
| 354 |
+
def refresh(self, request):
|
| 355 |
+
if self._universe_domain != credentials.DEFAULT_UNIVERSE_DOMAIN:
|
| 356 |
+
raise exceptions.RefreshError(
|
| 357 |
+
"User credential refresh is only supported in the default "
|
| 358 |
+
"googleapis.com universe domain, but the current universe "
|
| 359 |
+
"domain is {}. If you created the credential with an access "
|
| 360 |
+
"token, it's likely that the provided token is expired now, "
|
| 361 |
+
"please update your code with a valid token.".format(
|
| 362 |
+
self._universe_domain
|
| 363 |
+
)
|
| 364 |
+
)
|
| 365 |
+
|
| 366 |
+
scopes = self._scopes if self._scopes is not None else self._default_scopes
|
| 367 |
+
# Use refresh handler if available and no refresh token is
|
| 368 |
+
# available. This is useful in general when tokens are obtained by calling
|
| 369 |
+
# some external process on demand. It is particularly useful for retrieving
|
| 370 |
+
# downscoped tokens from a token broker.
|
| 371 |
+
if self._refresh_token is None and self.refresh_handler:
|
| 372 |
+
token, expiry = self.refresh_handler(request, scopes=scopes)
|
| 373 |
+
# Validate returned data.
|
| 374 |
+
if not isinstance(token, str):
|
| 375 |
+
raise exceptions.RefreshError(
|
| 376 |
+
"The refresh_handler returned token is not a string."
|
| 377 |
+
)
|
| 378 |
+
if not isinstance(expiry, datetime):
|
| 379 |
+
raise exceptions.RefreshError(
|
| 380 |
+
"The refresh_handler returned expiry is not a datetime object."
|
| 381 |
+
)
|
| 382 |
+
if _helpers.utcnow() >= expiry - _helpers.REFRESH_THRESHOLD:
|
| 383 |
+
raise exceptions.RefreshError(
|
| 384 |
+
"The credentials returned by the refresh_handler are "
|
| 385 |
+
"already expired."
|
| 386 |
+
)
|
| 387 |
+
self.token = token
|
| 388 |
+
self.expiry = expiry
|
| 389 |
+
return
|
| 390 |
+
|
| 391 |
+
if (
|
| 392 |
+
self._refresh_token is None
|
| 393 |
+
or self._token_uri is None
|
| 394 |
+
or self._client_id is None
|
| 395 |
+
or self._client_secret is None
|
| 396 |
+
):
|
| 397 |
+
raise exceptions.RefreshError(
|
| 398 |
+
"The credentials do not contain the necessary fields need to "
|
| 399 |
+
"refresh the access token. You must specify refresh_token, "
|
| 400 |
+
"token_uri, client_id, and client_secret."
|
| 401 |
+
)
|
| 402 |
+
|
| 403 |
+
(
|
| 404 |
+
access_token,
|
| 405 |
+
refresh_token,
|
| 406 |
+
expiry,
|
| 407 |
+
grant_response,
|
| 408 |
+
rapt_token,
|
| 409 |
+
) = reauth.refresh_grant(
|
| 410 |
+
request,
|
| 411 |
+
self._token_uri,
|
| 412 |
+
self._refresh_token,
|
| 413 |
+
self._client_id,
|
| 414 |
+
self._client_secret,
|
| 415 |
+
scopes=scopes,
|
| 416 |
+
rapt_token=self._rapt_token,
|
| 417 |
+
enable_reauth_refresh=self._enable_reauth_refresh,
|
| 418 |
+
)
|
| 419 |
+
|
| 420 |
+
self.token = access_token
|
| 421 |
+
self.expiry = expiry
|
| 422 |
+
self._refresh_token = refresh_token
|
| 423 |
+
self._id_token = grant_response.get("id_token")
|
| 424 |
+
self._rapt_token = rapt_token
|
| 425 |
+
|
| 426 |
+
if scopes and "scope" in grant_response:
|
| 427 |
+
requested_scopes = frozenset(scopes)
|
| 428 |
+
self._granted_scopes = grant_response["scope"].split()
|
| 429 |
+
granted_scopes = frozenset(self._granted_scopes)
|
| 430 |
+
scopes_requested_but_not_granted = requested_scopes - granted_scopes
|
| 431 |
+
if scopes_requested_but_not_granted:
|
| 432 |
+
# User might be presented with unbundled scopes at the time of
|
| 433 |
+
# consent. So it is a valid scenario to not have all the requested
|
| 434 |
+
# scopes as part of granted scopes but log a warning in case the
|
| 435 |
+
# developer wants to debug the scenario.
|
| 436 |
+
_LOGGER.warning(
|
| 437 |
+
"Not all requested scopes were granted by the "
|
| 438 |
+
"authorization server, missing scopes {}.".format(
|
| 439 |
+
", ".join(scopes_requested_but_not_granted)
|
| 440 |
+
)
|
| 441 |
+
)
|
| 442 |
+
|
| 443 |
+
@classmethod
|
| 444 |
+
def from_authorized_user_info(cls, info, scopes=None):
|
| 445 |
+
"""Creates a Credentials instance from parsed authorized user info.
|
| 446 |
+
|
| 447 |
+
Args:
|
| 448 |
+
info (Mapping[str, str]): The authorized user info in Google
|
| 449 |
+
format.
|
| 450 |
+
scopes (Sequence[str]): Optional list of scopes to include in the
|
| 451 |
+
credentials.
|
| 452 |
+
|
| 453 |
+
Returns:
|
| 454 |
+
google.oauth2.credentials.Credentials: The constructed
|
| 455 |
+
credentials.
|
| 456 |
+
|
| 457 |
+
Raises:
|
| 458 |
+
ValueError: If the info is not in the expected format.
|
| 459 |
+
"""
|
| 460 |
+
keys_needed = set(("refresh_token", "client_id", "client_secret"))
|
| 461 |
+
missing = keys_needed.difference(info.keys())
|
| 462 |
+
|
| 463 |
+
if missing:
|
| 464 |
+
raise ValueError(
|
| 465 |
+
"Authorized user info was not in the expected format, missing "
|
| 466 |
+
"fields {}.".format(", ".join(missing))
|
| 467 |
+
)
|
| 468 |
+
|
| 469 |
+
# access token expiry (datetime obj); auto-expire if not saved
|
| 470 |
+
expiry = info.get("expiry")
|
| 471 |
+
if expiry:
|
| 472 |
+
expiry = datetime.strptime(
|
| 473 |
+
expiry.rstrip("Z").split(".")[0], "%Y-%m-%dT%H:%M:%S"
|
| 474 |
+
)
|
| 475 |
+
else:
|
| 476 |
+
expiry = _helpers.utcnow() - _helpers.REFRESH_THRESHOLD
|
| 477 |
+
|
| 478 |
+
# process scopes, which needs to be a seq
|
| 479 |
+
if scopes is None and "scopes" in info:
|
| 480 |
+
scopes = info.get("scopes")
|
| 481 |
+
if isinstance(scopes, str):
|
| 482 |
+
scopes = scopes.split(" ")
|
| 483 |
+
|
| 484 |
+
return cls(
|
| 485 |
+
token=info.get("token"),
|
| 486 |
+
refresh_token=info.get("refresh_token"),
|
| 487 |
+
token_uri=_GOOGLE_OAUTH2_TOKEN_ENDPOINT, # always overrides
|
| 488 |
+
scopes=scopes,
|
| 489 |
+
client_id=info.get("client_id"),
|
| 490 |
+
client_secret=info.get("client_secret"),
|
| 491 |
+
quota_project_id=info.get("quota_project_id"), # may not exist
|
| 492 |
+
expiry=expiry,
|
| 493 |
+
rapt_token=info.get("rapt_token"), # may not exist
|
| 494 |
+
trust_boundary=info.get("trust_boundary"), # may not exist
|
| 495 |
+
universe_domain=info.get("universe_domain"), # may not exist
|
| 496 |
+
account=info.get("account", ""), # may not exist
|
| 497 |
+
)
|
| 498 |
+
|
| 499 |
+
@classmethod
|
| 500 |
+
def from_authorized_user_file(cls, filename, scopes=None):
|
| 501 |
+
"""Creates a Credentials instance from an authorized user json file.
|
| 502 |
+
|
| 503 |
+
Args:
|
| 504 |
+
filename (str): The path to the authorized user json file.
|
| 505 |
+
scopes (Sequence[str]): Optional list of scopes to include in the
|
| 506 |
+
credentials.
|
| 507 |
+
|
| 508 |
+
Returns:
|
| 509 |
+
google.oauth2.credentials.Credentials: The constructed
|
| 510 |
+
credentials.
|
| 511 |
+
|
| 512 |
+
Raises:
|
| 513 |
+
ValueError: If the file is not in the expected format.
|
| 514 |
+
"""
|
| 515 |
+
with io.open(filename, "r", encoding="utf-8") as json_file:
|
| 516 |
+
data = json.load(json_file)
|
| 517 |
+
return cls.from_authorized_user_info(data, scopes)
|
| 518 |
+
|
| 519 |
+
def to_json(self, strip=None):
|
| 520 |
+
"""Utility function that creates a JSON representation of a Credentials
|
| 521 |
+
object.
|
| 522 |
+
|
| 523 |
+
Args:
|
| 524 |
+
strip (Sequence[str]): Optional list of members to exclude from the
|
| 525 |
+
generated JSON.
|
| 526 |
+
|
| 527 |
+
Returns:
|
| 528 |
+
str: A JSON representation of this instance. When converted into
|
| 529 |
+
a dictionary, it can be passed to from_authorized_user_info()
|
| 530 |
+
to create a new credential instance.
|
| 531 |
+
"""
|
| 532 |
+
prep = {
|
| 533 |
+
"token": self.token,
|
| 534 |
+
"refresh_token": self.refresh_token,
|
| 535 |
+
"token_uri": self.token_uri,
|
| 536 |
+
"client_id": self.client_id,
|
| 537 |
+
"client_secret": self.client_secret,
|
| 538 |
+
"scopes": self.scopes,
|
| 539 |
+
"rapt_token": self.rapt_token,
|
| 540 |
+
"universe_domain": self._universe_domain,
|
| 541 |
+
"account": self._account,
|
| 542 |
+
}
|
| 543 |
+
if self.expiry: # flatten expiry timestamp
|
| 544 |
+
prep["expiry"] = self.expiry.isoformat() + "Z"
|
| 545 |
+
|
| 546 |
+
# Remove empty entries (those which are None)
|
| 547 |
+
prep = {k: v for k, v in prep.items() if v is not None}
|
| 548 |
+
|
| 549 |
+
# Remove entries that explicitely need to be removed
|
| 550 |
+
if strip is not None:
|
| 551 |
+
prep = {k: v for k, v in prep.items() if k not in strip}
|
| 552 |
+
|
| 553 |
+
return json.dumps(prep)
|
| 554 |
+
|
| 555 |
+
|
| 556 |
+
class UserAccessTokenCredentials(credentials.CredentialsWithQuotaProject):
|
| 557 |
+
"""Access token credentials for user account.
|
| 558 |
+
|
| 559 |
+
Obtain the access token for a given user account or the current active
|
| 560 |
+
user account with the ``gcloud auth print-access-token`` command.
|
| 561 |
+
|
| 562 |
+
Args:
|
| 563 |
+
account (Optional[str]): Account to get the access token for. If not
|
| 564 |
+
specified, the current active account will be used.
|
| 565 |
+
quota_project_id (Optional[str]): The project ID used for quota
|
| 566 |
+
and billing.
|
| 567 |
+
"""
|
| 568 |
+
|
| 569 |
+
def __init__(self, account=None, quota_project_id=None):
|
| 570 |
+
warnings.warn(
|
| 571 |
+
"UserAccessTokenCredentials is deprecated, please use "
|
| 572 |
+
"google.oauth2.credentials.Credentials instead. To use "
|
| 573 |
+
"that credential type, simply run "
|
| 574 |
+
"`gcloud auth application-default login` and let the "
|
| 575 |
+
"client libraries pick up the application default credentials."
|
| 576 |
+
)
|
| 577 |
+
super(UserAccessTokenCredentials, self).__init__()
|
| 578 |
+
self._account = account
|
| 579 |
+
self._quota_project_id = quota_project_id
|
| 580 |
+
|
| 581 |
+
def with_account(self, account):
|
| 582 |
+
"""Create a new instance with the given account.
|
| 583 |
+
|
| 584 |
+
Args:
|
| 585 |
+
account (str): Account to get the access token for.
|
| 586 |
+
|
| 587 |
+
Returns:
|
| 588 |
+
google.oauth2.credentials.UserAccessTokenCredentials: The created
|
| 589 |
+
credentials with the given account.
|
| 590 |
+
"""
|
| 591 |
+
return self.__class__(account=account, quota_project_id=self._quota_project_id)
|
| 592 |
+
|
| 593 |
+
@_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
|
| 594 |
+
def with_quota_project(self, quota_project_id):
|
| 595 |
+
return self.__class__(account=self._account, quota_project_id=quota_project_id)
|
| 596 |
+
|
| 597 |
+
def refresh(self, request):
|
| 598 |
+
"""Refreshes the access token.
|
| 599 |
+
|
| 600 |
+
Args:
|
| 601 |
+
request (google.auth.transport.Request): This argument is required
|
| 602 |
+
by the base class interface but not used in this implementation,
|
| 603 |
+
so just set it to `None`.
|
| 604 |
+
|
| 605 |
+
Raises:
|
| 606 |
+
google.auth.exceptions.UserAccessTokenError: If the access token
|
| 607 |
+
refresh failed.
|
| 608 |
+
"""
|
| 609 |
+
self.token = _cloud_sdk.get_auth_access_token(self._account)
|
| 610 |
+
|
| 611 |
+
@_helpers.copy_docstring(credentials.Credentials)
|
| 612 |
+
def before_request(self, request, method, url, headers):
|
| 613 |
+
self.refresh(request)
|
| 614 |
+
self.apply(headers)
|
.venv/lib/python3.11/site-packages/google/oauth2/gdch_credentials.py
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2022 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""Experimental GDCH credentials support.
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
import datetime
|
| 19 |
+
|
| 20 |
+
from google.auth import _helpers
|
| 21 |
+
from google.auth import _service_account_info
|
| 22 |
+
from google.auth import credentials
|
| 23 |
+
from google.auth import exceptions
|
| 24 |
+
from google.auth import jwt
|
| 25 |
+
from google.oauth2 import _client
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
TOKEN_EXCHANGE_TYPE = "urn:ietf:params:oauth:token-type:token-exchange"
|
| 29 |
+
ACCESS_TOKEN_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token"
|
| 30 |
+
SERVICE_ACCOUNT_TOKEN_TYPE = "urn:k8s:params:oauth:token-type:serviceaccount"
|
| 31 |
+
JWT_LIFETIME = datetime.timedelta(seconds=3600) # 1 hour
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class ServiceAccountCredentials(credentials.Credentials):
|
| 35 |
+
"""Credentials for GDCH (`Google Distributed Cloud Hosted`_) for service
|
| 36 |
+
account users.
|
| 37 |
+
|
| 38 |
+
.. _Google Distributed Cloud Hosted:
|
| 39 |
+
https://cloud.google.com/blog/topics/hybrid-cloud/\
|
| 40 |
+
announcing-google-distributed-cloud-edge-and-hosted
|
| 41 |
+
|
| 42 |
+
To create a GDCH service account credential, first create a JSON file of
|
| 43 |
+
the following format::
|
| 44 |
+
|
| 45 |
+
{
|
| 46 |
+
"type": "gdch_service_account",
|
| 47 |
+
"format_version": "1",
|
| 48 |
+
"project": "<project name>",
|
| 49 |
+
"private_key_id": "<key id>",
|
| 50 |
+
"private_key": "-----BEGIN EC PRIVATE KEY-----\n<key bytes>\n-----END EC PRIVATE KEY-----\n",
|
| 51 |
+
"name": "<service identity name>",
|
| 52 |
+
"ca_cert_path": "<CA cert path>",
|
| 53 |
+
"token_uri": "https://service-identity.<Domain>/authenticate"
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
The "format_version" field stands for the format of the JSON file. For now
|
| 57 |
+
it is always "1". The `private_key_id` and `private_key` is used for signing.
|
| 58 |
+
The `ca_cert_path` is used for token server TLS certificate verification.
|
| 59 |
+
|
| 60 |
+
After the JSON file is created, set `GOOGLE_APPLICATION_CREDENTIALS` environment
|
| 61 |
+
variable to the JSON file path, then use the following code to create the
|
| 62 |
+
credential::
|
| 63 |
+
|
| 64 |
+
import google.auth
|
| 65 |
+
|
| 66 |
+
credential, _ = google.auth.default()
|
| 67 |
+
credential = credential.with_gdch_audience("<the audience>")
|
| 68 |
+
|
| 69 |
+
We can also create the credential directly::
|
| 70 |
+
|
| 71 |
+
from google.oauth import gdch_credentials
|
| 72 |
+
|
| 73 |
+
credential = gdch_credentials.ServiceAccountCredentials.from_service_account_file("<the json file path>")
|
| 74 |
+
credential = credential.with_gdch_audience("<the audience>")
|
| 75 |
+
|
| 76 |
+
The token is obtained in the following way. This class first creates a
|
| 77 |
+
self signed JWT. It uses the `name` value as the `iss` and `sub` claim, and
|
| 78 |
+
the `token_uri` as the `aud` claim, and signs the JWT with the `private_key`.
|
| 79 |
+
It then sends the JWT to the `token_uri` to exchange a final token for
|
| 80 |
+
`audience`.
|
| 81 |
+
"""
|
| 82 |
+
|
| 83 |
+
def __init__(
|
| 84 |
+
self, signer, service_identity_name, project, audience, token_uri, ca_cert_path
|
| 85 |
+
):
|
| 86 |
+
"""
|
| 87 |
+
Args:
|
| 88 |
+
signer (google.auth.crypt.Signer): The signer used to sign JWTs.
|
| 89 |
+
service_identity_name (str): The service identity name. It will be
|
| 90 |
+
used as the `iss` and `sub` claim in the self signed JWT.
|
| 91 |
+
project (str): The project.
|
| 92 |
+
audience (str): The audience for the final token.
|
| 93 |
+
token_uri (str): The token server uri.
|
| 94 |
+
ca_cert_path (str): The CA cert path for token server side TLS
|
| 95 |
+
certificate verification. If the token server uses well known
|
| 96 |
+
CA, then this parameter can be `None`.
|
| 97 |
+
"""
|
| 98 |
+
super(ServiceAccountCredentials, self).__init__()
|
| 99 |
+
self._signer = signer
|
| 100 |
+
self._service_identity_name = service_identity_name
|
| 101 |
+
self._project = project
|
| 102 |
+
self._audience = audience
|
| 103 |
+
self._token_uri = token_uri
|
| 104 |
+
self._ca_cert_path = ca_cert_path
|
| 105 |
+
|
| 106 |
+
def _create_jwt(self):
|
| 107 |
+
now = _helpers.utcnow()
|
| 108 |
+
expiry = now + JWT_LIFETIME
|
| 109 |
+
iss_sub_value = "system:serviceaccount:{}:{}".format(
|
| 110 |
+
self._project, self._service_identity_name
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
payload = {
|
| 114 |
+
"iss": iss_sub_value,
|
| 115 |
+
"sub": iss_sub_value,
|
| 116 |
+
"aud": self._token_uri,
|
| 117 |
+
"iat": _helpers.datetime_to_secs(now),
|
| 118 |
+
"exp": _helpers.datetime_to_secs(expiry),
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
return _helpers.from_bytes(jwt.encode(self._signer, payload))
|
| 122 |
+
|
| 123 |
+
@_helpers.copy_docstring(credentials.Credentials)
|
| 124 |
+
def refresh(self, request):
|
| 125 |
+
import google.auth.transport.requests
|
| 126 |
+
|
| 127 |
+
if not isinstance(request, google.auth.transport.requests.Request):
|
| 128 |
+
raise exceptions.RefreshError(
|
| 129 |
+
"For GDCH service account credentials, request must be a google.auth.transport.requests.Request object"
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
# Create a self signed JWT, and do token exchange.
|
| 133 |
+
jwt_token = self._create_jwt()
|
| 134 |
+
request_body = {
|
| 135 |
+
"grant_type": TOKEN_EXCHANGE_TYPE,
|
| 136 |
+
"audience": self._audience,
|
| 137 |
+
"requested_token_type": ACCESS_TOKEN_TOKEN_TYPE,
|
| 138 |
+
"subject_token": jwt_token,
|
| 139 |
+
"subject_token_type": SERVICE_ACCOUNT_TOKEN_TYPE,
|
| 140 |
+
}
|
| 141 |
+
response_data = _client._token_endpoint_request(
|
| 142 |
+
request,
|
| 143 |
+
self._token_uri,
|
| 144 |
+
request_body,
|
| 145 |
+
access_token=None,
|
| 146 |
+
use_json=True,
|
| 147 |
+
verify=self._ca_cert_path,
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
self.token, _, self.expiry, _ = _client._handle_refresh_grant_response(
|
| 151 |
+
response_data, None
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
def with_gdch_audience(self, audience):
|
| 155 |
+
"""Create a copy of GDCH credentials with the specified audience.
|
| 156 |
+
|
| 157 |
+
Args:
|
| 158 |
+
audience (str): The intended audience for GDCH credentials.
|
| 159 |
+
"""
|
| 160 |
+
return self.__class__(
|
| 161 |
+
self._signer,
|
| 162 |
+
self._service_identity_name,
|
| 163 |
+
self._project,
|
| 164 |
+
audience,
|
| 165 |
+
self._token_uri,
|
| 166 |
+
self._ca_cert_path,
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
@classmethod
|
| 170 |
+
def _from_signer_and_info(cls, signer, info):
|
| 171 |
+
"""Creates a Credentials instance from a signer and service account
|
| 172 |
+
info.
|
| 173 |
+
|
| 174 |
+
Args:
|
| 175 |
+
signer (google.auth.crypt.Signer): The signer used to sign JWTs.
|
| 176 |
+
info (Mapping[str, str]): The service account info.
|
| 177 |
+
|
| 178 |
+
Returns:
|
| 179 |
+
google.oauth2.gdch_credentials.ServiceAccountCredentials: The constructed
|
| 180 |
+
credentials.
|
| 181 |
+
|
| 182 |
+
Raises:
|
| 183 |
+
ValueError: If the info is not in the expected format.
|
| 184 |
+
"""
|
| 185 |
+
if info["format_version"] != "1":
|
| 186 |
+
raise ValueError("Only format version 1 is supported")
|
| 187 |
+
|
| 188 |
+
return cls(
|
| 189 |
+
signer,
|
| 190 |
+
info["name"], # service_identity_name
|
| 191 |
+
info["project"],
|
| 192 |
+
None, # audience
|
| 193 |
+
info["token_uri"],
|
| 194 |
+
info.get("ca_cert_path", None),
|
| 195 |
+
)
|
| 196 |
+
|
| 197 |
+
@classmethod
|
| 198 |
+
def from_service_account_info(cls, info):
|
| 199 |
+
"""Creates a Credentials instance from parsed service account info.
|
| 200 |
+
|
| 201 |
+
Args:
|
| 202 |
+
info (Mapping[str, str]): The service account info in Google
|
| 203 |
+
format.
|
| 204 |
+
kwargs: Additional arguments to pass to the constructor.
|
| 205 |
+
|
| 206 |
+
Returns:
|
| 207 |
+
google.oauth2.gdch_credentials.ServiceAccountCredentials: The constructed
|
| 208 |
+
credentials.
|
| 209 |
+
|
| 210 |
+
Raises:
|
| 211 |
+
ValueError: If the info is not in the expected format.
|
| 212 |
+
"""
|
| 213 |
+
signer = _service_account_info.from_dict(
|
| 214 |
+
info,
|
| 215 |
+
require=[
|
| 216 |
+
"format_version",
|
| 217 |
+
"private_key_id",
|
| 218 |
+
"private_key",
|
| 219 |
+
"name",
|
| 220 |
+
"project",
|
| 221 |
+
"token_uri",
|
| 222 |
+
],
|
| 223 |
+
use_rsa_signer=False,
|
| 224 |
+
)
|
| 225 |
+
return cls._from_signer_and_info(signer, info)
|
| 226 |
+
|
| 227 |
+
@classmethod
|
| 228 |
+
def from_service_account_file(cls, filename):
|
| 229 |
+
"""Creates a Credentials instance from a service account json file.
|
| 230 |
+
|
| 231 |
+
Args:
|
| 232 |
+
filename (str): The path to the service account json file.
|
| 233 |
+
kwargs: Additional arguments to pass to the constructor.
|
| 234 |
+
|
| 235 |
+
Returns:
|
| 236 |
+
google.oauth2.gdch_credentials.ServiceAccountCredentials: The constructed
|
| 237 |
+
credentials.
|
| 238 |
+
"""
|
| 239 |
+
info, signer = _service_account_info.from_filename(
|
| 240 |
+
filename,
|
| 241 |
+
require=[
|
| 242 |
+
"format_version",
|
| 243 |
+
"private_key_id",
|
| 244 |
+
"private_key",
|
| 245 |
+
"name",
|
| 246 |
+
"project",
|
| 247 |
+
"token_uri",
|
| 248 |
+
],
|
| 249 |
+
use_rsa_signer=False,
|
| 250 |
+
)
|
| 251 |
+
return cls._from_signer_and_info(signer, info)
|
.venv/lib/python3.11/site-packages/google/oauth2/id_token.py
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2016 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""Google ID Token helpers.
|
| 16 |
+
|
| 17 |
+
Provides support for verifying `OpenID Connect ID Tokens`_, especially ones
|
| 18 |
+
generated by Google infrastructure.
|
| 19 |
+
|
| 20 |
+
To parse and verify an ID Token issued by Google's OAuth 2.0 authorization
|
| 21 |
+
server use :func:`verify_oauth2_token`. To verify an ID Token issued by
|
| 22 |
+
Firebase, use :func:`verify_firebase_token`.
|
| 23 |
+
|
| 24 |
+
A general purpose ID Token verifier is available as :func:`verify_token`.
|
| 25 |
+
|
| 26 |
+
Example::
|
| 27 |
+
|
| 28 |
+
from google.oauth2 import id_token
|
| 29 |
+
from google.auth.transport import requests
|
| 30 |
+
|
| 31 |
+
request = requests.Request()
|
| 32 |
+
|
| 33 |
+
id_info = id_token.verify_oauth2_token(
|
| 34 |
+
token, request, 'my-client-id.example.com')
|
| 35 |
+
|
| 36 |
+
userid = id_info['sub']
|
| 37 |
+
|
| 38 |
+
By default, this will re-fetch certificates for each verification. Because
|
| 39 |
+
Google's public keys are only changed infrequently (on the order of once per
|
| 40 |
+
day), you may wish to take advantage of caching to reduce latency and the
|
| 41 |
+
potential for network errors. This can be accomplished using an external
|
| 42 |
+
library like `CacheControl`_ to create a cache-aware
|
| 43 |
+
:class:`google.auth.transport.Request`::
|
| 44 |
+
|
| 45 |
+
import cachecontrol
|
| 46 |
+
import google.auth.transport.requests
|
| 47 |
+
import requests
|
| 48 |
+
|
| 49 |
+
session = requests.session()
|
| 50 |
+
cached_session = cachecontrol.CacheControl(session)
|
| 51 |
+
request = google.auth.transport.requests.Request(session=cached_session)
|
| 52 |
+
|
| 53 |
+
.. _OpenID Connect ID Tokens:
|
| 54 |
+
http://openid.net/specs/openid-connect-core-1_0.html#IDToken
|
| 55 |
+
.. _CacheControl: https://cachecontrol.readthedocs.io
|
| 56 |
+
"""
|
| 57 |
+
|
| 58 |
+
import http.client as http_client
|
| 59 |
+
import json
|
| 60 |
+
import os
|
| 61 |
+
|
| 62 |
+
from google.auth import environment_vars
|
| 63 |
+
from google.auth import exceptions
|
| 64 |
+
from google.auth import jwt
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
# The URL that provides public certificates for verifying ID tokens issued
|
| 68 |
+
# by Google's OAuth 2.0 authorization server.
|
| 69 |
+
_GOOGLE_OAUTH2_CERTS_URL = "https://www.googleapis.com/oauth2/v1/certs"
|
| 70 |
+
|
| 71 |
+
# The URL that provides public certificates for verifying ID tokens issued
|
| 72 |
+
# by Firebase and the Google APIs infrastructure
|
| 73 |
+
_GOOGLE_APIS_CERTS_URL = (
|
| 74 |
+
"https://www.googleapis.com/robot/v1/metadata/x509"
|
| 75 |
+
"/securetoken@system.gserviceaccount.com"
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
_GOOGLE_ISSUERS = ["accounts.google.com", "https://accounts.google.com"]
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def _fetch_certs(request, certs_url):
|
| 82 |
+
"""Fetches certificates.
|
| 83 |
+
|
| 84 |
+
Google-style cerificate endpoints return JSON in the format of
|
| 85 |
+
``{'key id': 'x509 certificate'}`` or a certificate array according
|
| 86 |
+
to the JWK spec (see https://tools.ietf.org/html/rfc7517).
|
| 87 |
+
|
| 88 |
+
Args:
|
| 89 |
+
request (google.auth.transport.Request): The object used to make
|
| 90 |
+
HTTP requests.
|
| 91 |
+
certs_url (str): The certificate endpoint URL.
|
| 92 |
+
|
| 93 |
+
Returns:
|
| 94 |
+
Mapping[str, str] | Mapping[str, list]: A mapping of public keys
|
| 95 |
+
in x.509 or JWK spec.
|
| 96 |
+
"""
|
| 97 |
+
response = request(certs_url, method="GET")
|
| 98 |
+
|
| 99 |
+
if response.status != http_client.OK:
|
| 100 |
+
raise exceptions.TransportError(
|
| 101 |
+
"Could not fetch certificates at {}".format(certs_url)
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
+
return json.loads(response.data.decode("utf-8"))
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def verify_token(
|
| 108 |
+
id_token,
|
| 109 |
+
request,
|
| 110 |
+
audience=None,
|
| 111 |
+
certs_url=_GOOGLE_OAUTH2_CERTS_URL,
|
| 112 |
+
clock_skew_in_seconds=0,
|
| 113 |
+
):
|
| 114 |
+
"""Verifies an ID token and returns the decoded token.
|
| 115 |
+
|
| 116 |
+
Args:
|
| 117 |
+
id_token (Union[str, bytes]): The encoded token.
|
| 118 |
+
request (google.auth.transport.Request): The object used to make
|
| 119 |
+
HTTP requests.
|
| 120 |
+
audience (str or list): The audience or audiences that this token is
|
| 121 |
+
intended for. If None then the audience is not verified.
|
| 122 |
+
certs_url (str): The URL that specifies the certificates to use to
|
| 123 |
+
verify the token. This URL should return JSON in the format of
|
| 124 |
+
``{'key id': 'x509 certificate'}`` or a certificate array according to
|
| 125 |
+
the JWK spec (see https://tools.ietf.org/html/rfc7517).
|
| 126 |
+
clock_skew_in_seconds (int): The clock skew used for `iat` and `exp`
|
| 127 |
+
validation.
|
| 128 |
+
|
| 129 |
+
Returns:
|
| 130 |
+
Mapping[str, Any]: The decoded token.
|
| 131 |
+
"""
|
| 132 |
+
certs = _fetch_certs(request, certs_url)
|
| 133 |
+
|
| 134 |
+
if "keys" in certs:
|
| 135 |
+
try:
|
| 136 |
+
import jwt as jwt_lib # type: ignore
|
| 137 |
+
except ImportError as caught_exc: # pragma: NO COVER
|
| 138 |
+
raise ImportError(
|
| 139 |
+
"The pyjwt library is not installed, please install the pyjwt package to use the jwk certs format."
|
| 140 |
+
) from caught_exc
|
| 141 |
+
jwks_client = jwt_lib.PyJWKClient(certs_url)
|
| 142 |
+
signing_key = jwks_client.get_signing_key_from_jwt(id_token)
|
| 143 |
+
return jwt_lib.decode(
|
| 144 |
+
id_token,
|
| 145 |
+
signing_key.key,
|
| 146 |
+
algorithms=[signing_key.algorithm_name],
|
| 147 |
+
audience=audience,
|
| 148 |
+
)
|
| 149 |
+
else:
|
| 150 |
+
return jwt.decode(
|
| 151 |
+
id_token,
|
| 152 |
+
certs=certs,
|
| 153 |
+
audience=audience,
|
| 154 |
+
clock_skew_in_seconds=clock_skew_in_seconds,
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def verify_oauth2_token(id_token, request, audience=None, clock_skew_in_seconds=0):
|
| 159 |
+
"""Verifies an ID Token issued by Google's OAuth 2.0 authorization server.
|
| 160 |
+
|
| 161 |
+
Args:
|
| 162 |
+
id_token (Union[str, bytes]): The encoded token.
|
| 163 |
+
request (google.auth.transport.Request): The object used to make
|
| 164 |
+
HTTP requests.
|
| 165 |
+
audience (str): The audience that this token is intended for. This is
|
| 166 |
+
typically your application's OAuth 2.0 client ID. If None then the
|
| 167 |
+
audience is not verified.
|
| 168 |
+
clock_skew_in_seconds (int): The clock skew used for `iat` and `exp`
|
| 169 |
+
validation.
|
| 170 |
+
|
| 171 |
+
Returns:
|
| 172 |
+
Mapping[str, Any]: The decoded token.
|
| 173 |
+
|
| 174 |
+
Raises:
|
| 175 |
+
exceptions.GoogleAuthError: If the issuer is invalid.
|
| 176 |
+
ValueError: If token verification fails
|
| 177 |
+
"""
|
| 178 |
+
idinfo = verify_token(
|
| 179 |
+
id_token,
|
| 180 |
+
request,
|
| 181 |
+
audience=audience,
|
| 182 |
+
certs_url=_GOOGLE_OAUTH2_CERTS_URL,
|
| 183 |
+
clock_skew_in_seconds=clock_skew_in_seconds,
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
if idinfo["iss"] not in _GOOGLE_ISSUERS:
|
| 187 |
+
raise exceptions.GoogleAuthError(
|
| 188 |
+
"Wrong issuer. 'iss' should be one of the following: {}".format(
|
| 189 |
+
_GOOGLE_ISSUERS
|
| 190 |
+
)
|
| 191 |
+
)
|
| 192 |
+
|
| 193 |
+
return idinfo
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
def verify_firebase_token(id_token, request, audience=None, clock_skew_in_seconds=0):
|
| 197 |
+
"""Verifies an ID Token issued by Firebase Authentication.
|
| 198 |
+
|
| 199 |
+
Args:
|
| 200 |
+
id_token (Union[str, bytes]): The encoded token.
|
| 201 |
+
request (google.auth.transport.Request): The object used to make
|
| 202 |
+
HTTP requests.
|
| 203 |
+
audience (str): The audience that this token is intended for. This is
|
| 204 |
+
typically your Firebase application ID. If None then the audience
|
| 205 |
+
is not verified.
|
| 206 |
+
clock_skew_in_seconds (int): The clock skew used for `iat` and `exp`
|
| 207 |
+
validation.
|
| 208 |
+
|
| 209 |
+
Returns:
|
| 210 |
+
Mapping[str, Any]: The decoded token.
|
| 211 |
+
"""
|
| 212 |
+
return verify_token(
|
| 213 |
+
id_token,
|
| 214 |
+
request,
|
| 215 |
+
audience=audience,
|
| 216 |
+
certs_url=_GOOGLE_APIS_CERTS_URL,
|
| 217 |
+
clock_skew_in_seconds=clock_skew_in_seconds,
|
| 218 |
+
)
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
def fetch_id_token_credentials(audience, request=None):
|
| 222 |
+
"""Create the ID Token credentials from the current environment.
|
| 223 |
+
|
| 224 |
+
This function acquires ID token from the environment in the following order.
|
| 225 |
+
See https://google.aip.dev/auth/4110.
|
| 226 |
+
|
| 227 |
+
1. If the environment variable ``GOOGLE_APPLICATION_CREDENTIALS`` is set
|
| 228 |
+
to the path of a valid service account JSON file, then ID token is
|
| 229 |
+
acquired using this service account credentials.
|
| 230 |
+
2. If the application is running in Compute Engine, App Engine or Cloud Run,
|
| 231 |
+
then the ID token are obtained from the metadata server.
|
| 232 |
+
3. If metadata server doesn't exist and no valid service account credentials
|
| 233 |
+
are found, :class:`~google.auth.exceptions.DefaultCredentialsError` will
|
| 234 |
+
be raised.
|
| 235 |
+
|
| 236 |
+
Example::
|
| 237 |
+
|
| 238 |
+
import google.oauth2.id_token
|
| 239 |
+
import google.auth.transport.requests
|
| 240 |
+
|
| 241 |
+
request = google.auth.transport.requests.Request()
|
| 242 |
+
target_audience = "https://pubsub.googleapis.com"
|
| 243 |
+
|
| 244 |
+
# Create ID token credentials.
|
| 245 |
+
credentials = google.oauth2.id_token.fetch_id_token_credentials(target_audience, request=request)
|
| 246 |
+
|
| 247 |
+
# Refresh the credential to obtain an ID token.
|
| 248 |
+
credentials.refresh(request)
|
| 249 |
+
|
| 250 |
+
id_token = credentials.token
|
| 251 |
+
id_token_expiry = credentials.expiry
|
| 252 |
+
|
| 253 |
+
Args:
|
| 254 |
+
audience (str): The audience that this ID token is intended for.
|
| 255 |
+
request (Optional[google.auth.transport.Request]): A callable used to make
|
| 256 |
+
HTTP requests. A request object will be created if not provided.
|
| 257 |
+
|
| 258 |
+
Returns:
|
| 259 |
+
google.auth.credentials.Credentials: The ID token credentials.
|
| 260 |
+
|
| 261 |
+
Raises:
|
| 262 |
+
~google.auth.exceptions.DefaultCredentialsError:
|
| 263 |
+
If metadata server doesn't exist and no valid service account
|
| 264 |
+
credentials are found.
|
| 265 |
+
"""
|
| 266 |
+
# 1. Try to get credentials from the GOOGLE_APPLICATION_CREDENTIALS environment
|
| 267 |
+
# variable.
|
| 268 |
+
credentials_filename = os.environ.get(environment_vars.CREDENTIALS)
|
| 269 |
+
if credentials_filename:
|
| 270 |
+
if not (
|
| 271 |
+
os.path.exists(credentials_filename)
|
| 272 |
+
and os.path.isfile(credentials_filename)
|
| 273 |
+
):
|
| 274 |
+
raise exceptions.DefaultCredentialsError(
|
| 275 |
+
"GOOGLE_APPLICATION_CREDENTIALS path is either not found or invalid."
|
| 276 |
+
)
|
| 277 |
+
|
| 278 |
+
try:
|
| 279 |
+
with open(credentials_filename, "r") as f:
|
| 280 |
+
from google.oauth2 import service_account
|
| 281 |
+
|
| 282 |
+
info = json.load(f)
|
| 283 |
+
if info.get("type") == "service_account":
|
| 284 |
+
return service_account.IDTokenCredentials.from_service_account_info(
|
| 285 |
+
info, target_audience=audience
|
| 286 |
+
)
|
| 287 |
+
except ValueError as caught_exc:
|
| 288 |
+
new_exc = exceptions.DefaultCredentialsError(
|
| 289 |
+
"GOOGLE_APPLICATION_CREDENTIALS is not valid service account credentials.",
|
| 290 |
+
caught_exc,
|
| 291 |
+
)
|
| 292 |
+
raise new_exc from caught_exc
|
| 293 |
+
|
| 294 |
+
# 2. Try to fetch ID token from metada server if it exists. The code
|
| 295 |
+
# works for GAE and Cloud Run metadata server as well.
|
| 296 |
+
try:
|
| 297 |
+
from google.auth import compute_engine
|
| 298 |
+
from google.auth.compute_engine import _metadata
|
| 299 |
+
|
| 300 |
+
# Create a request object if not provided.
|
| 301 |
+
if not request:
|
| 302 |
+
import google.auth.transport.requests
|
| 303 |
+
|
| 304 |
+
request = google.auth.transport.requests.Request()
|
| 305 |
+
|
| 306 |
+
if _metadata.ping(request):
|
| 307 |
+
return compute_engine.IDTokenCredentials(
|
| 308 |
+
request, audience, use_metadata_identity_endpoint=True
|
| 309 |
+
)
|
| 310 |
+
except (ImportError, exceptions.TransportError):
|
| 311 |
+
pass
|
| 312 |
+
|
| 313 |
+
raise exceptions.DefaultCredentialsError(
|
| 314 |
+
"Neither metadata server or valid service account credentials are found."
|
| 315 |
+
)
|
| 316 |
+
|
| 317 |
+
|
| 318 |
+
def fetch_id_token(request, audience):
|
| 319 |
+
"""Fetch the ID Token from the current environment.
|
| 320 |
+
|
| 321 |
+
This function acquires ID token from the environment in the following order.
|
| 322 |
+
See https://google.aip.dev/auth/4110.
|
| 323 |
+
|
| 324 |
+
1. If the environment variable ``GOOGLE_APPLICATION_CREDENTIALS`` is set
|
| 325 |
+
to the path of a valid service account JSON file, then ID token is
|
| 326 |
+
acquired using this service account credentials.
|
| 327 |
+
2. If the application is running in Compute Engine, App Engine or Cloud Run,
|
| 328 |
+
then the ID token are obtained from the metadata server.
|
| 329 |
+
3. If metadata server doesn't exist and no valid service account credentials
|
| 330 |
+
are found, :class:`~google.auth.exceptions.DefaultCredentialsError` will
|
| 331 |
+
be raised.
|
| 332 |
+
|
| 333 |
+
Example::
|
| 334 |
+
|
| 335 |
+
import google.oauth2.id_token
|
| 336 |
+
import google.auth.transport.requests
|
| 337 |
+
|
| 338 |
+
request = google.auth.transport.requests.Request()
|
| 339 |
+
target_audience = "https://pubsub.googleapis.com"
|
| 340 |
+
|
| 341 |
+
id_token = google.oauth2.id_token.fetch_id_token(request, target_audience)
|
| 342 |
+
|
| 343 |
+
Args:
|
| 344 |
+
request (google.auth.transport.Request): A callable used to make
|
| 345 |
+
HTTP requests.
|
| 346 |
+
audience (str): The audience that this ID token is intended for.
|
| 347 |
+
|
| 348 |
+
Returns:
|
| 349 |
+
str: The ID token.
|
| 350 |
+
|
| 351 |
+
Raises:
|
| 352 |
+
~google.auth.exceptions.DefaultCredentialsError:
|
| 353 |
+
If metadata server doesn't exist and no valid service account
|
| 354 |
+
credentials are found.
|
| 355 |
+
"""
|
| 356 |
+
id_token_credentials = fetch_id_token_credentials(audience, request=request)
|
| 357 |
+
id_token_credentials.refresh(request)
|
| 358 |
+
return id_token_credentials.token
|
.venv/lib/python3.11/site-packages/google/oauth2/py.typed
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Marker file for PEP 561.
|
| 2 |
+
# The google-oauth2 package uses inline types.
|
.venv/lib/python3.11/site-packages/google/oauth2/reauth.py
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2021 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""A module that provides functions for handling rapt authentication.
|
| 16 |
+
|
| 17 |
+
Reauth is a process of obtaining additional authentication (such as password,
|
| 18 |
+
security token, etc.) while refreshing OAuth 2.0 credentials for a user.
|
| 19 |
+
|
| 20 |
+
Credentials that use the Reauth flow must have the reauth scope,
|
| 21 |
+
``https://www.googleapis.com/auth/accounts.reauth``.
|
| 22 |
+
|
| 23 |
+
This module provides a high-level function for executing the Reauth process,
|
| 24 |
+
:func:`refresh_grant`, and lower-level helpers for doing the individual
|
| 25 |
+
steps of the reauth process.
|
| 26 |
+
|
| 27 |
+
Those steps are:
|
| 28 |
+
|
| 29 |
+
1. Obtaining a list of challenges from the reauth server.
|
| 30 |
+
2. Running through each challenge and sending the result back to the reauth
|
| 31 |
+
server.
|
| 32 |
+
3. Refreshing the access token using the returned rapt token.
|
| 33 |
+
"""
|
| 34 |
+
|
| 35 |
+
import sys
|
| 36 |
+
|
| 37 |
+
from google.auth import exceptions
|
| 38 |
+
from google.auth import metrics
|
| 39 |
+
from google.oauth2 import _client
|
| 40 |
+
from google.oauth2 import challenges
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
_REAUTH_SCOPE = "https://www.googleapis.com/auth/accounts.reauth"
|
| 44 |
+
_REAUTH_API = "https://reauth.googleapis.com/v2/sessions"
|
| 45 |
+
|
| 46 |
+
_REAUTH_NEEDED_ERROR = "invalid_grant"
|
| 47 |
+
_REAUTH_NEEDED_ERROR_INVALID_RAPT = "invalid_rapt"
|
| 48 |
+
_REAUTH_NEEDED_ERROR_RAPT_REQUIRED = "rapt_required"
|
| 49 |
+
|
| 50 |
+
_AUTHENTICATED = "AUTHENTICATED"
|
| 51 |
+
_CHALLENGE_REQUIRED = "CHALLENGE_REQUIRED"
|
| 52 |
+
_CHALLENGE_PENDING = "CHALLENGE_PENDING"
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
# Override this global variable to set custom max number of rounds of reauth
|
| 56 |
+
# challenges should be run.
|
| 57 |
+
RUN_CHALLENGE_RETRY_LIMIT = 5
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def is_interactive():
|
| 61 |
+
"""Check if we are in an interractive environment.
|
| 62 |
+
|
| 63 |
+
Override this function with a different logic if you are using this library
|
| 64 |
+
outside a CLI.
|
| 65 |
+
|
| 66 |
+
If the rapt token needs refreshing, the user needs to answer the challenges.
|
| 67 |
+
If the user is not in an interractive environment, the challenges can not
|
| 68 |
+
be answered and we just wait for timeout for no reason.
|
| 69 |
+
|
| 70 |
+
Returns:
|
| 71 |
+
bool: True if is interactive environment, False otherwise.
|
| 72 |
+
"""
|
| 73 |
+
|
| 74 |
+
return sys.stdin.isatty()
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def _get_challenges(
|
| 78 |
+
request, supported_challenge_types, access_token, requested_scopes=None
|
| 79 |
+
):
|
| 80 |
+
"""Does initial request to reauth API to get the challenges.
|
| 81 |
+
|
| 82 |
+
Args:
|
| 83 |
+
request (google.auth.transport.Request): A callable used to make
|
| 84 |
+
HTTP requests.
|
| 85 |
+
supported_challenge_types (Sequence[str]): list of challenge names
|
| 86 |
+
supported by the manager.
|
| 87 |
+
access_token (str): Access token with reauth scopes.
|
| 88 |
+
requested_scopes (Optional(Sequence[str])): Authorized scopes for the credentials.
|
| 89 |
+
|
| 90 |
+
Returns:
|
| 91 |
+
dict: The response from the reauth API.
|
| 92 |
+
"""
|
| 93 |
+
body = {"supportedChallengeTypes": supported_challenge_types}
|
| 94 |
+
if requested_scopes:
|
| 95 |
+
body["oauthScopesForDomainPolicyLookup"] = requested_scopes
|
| 96 |
+
metrics_header = {metrics.API_CLIENT_HEADER: metrics.reauth_start()}
|
| 97 |
+
|
| 98 |
+
return _client._token_endpoint_request(
|
| 99 |
+
request,
|
| 100 |
+
_REAUTH_API + ":start",
|
| 101 |
+
body,
|
| 102 |
+
access_token=access_token,
|
| 103 |
+
use_json=True,
|
| 104 |
+
headers=metrics_header,
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def _send_challenge_result(
|
| 109 |
+
request, session_id, challenge_id, client_input, access_token
|
| 110 |
+
):
|
| 111 |
+
"""Attempt to refresh access token by sending next challenge result.
|
| 112 |
+
|
| 113 |
+
Args:
|
| 114 |
+
request (google.auth.transport.Request): A callable used to make
|
| 115 |
+
HTTP requests.
|
| 116 |
+
session_id (str): session id returned by the initial reauth call.
|
| 117 |
+
challenge_id (str): challenge id returned by the initial reauth call.
|
| 118 |
+
client_input: dict with a challenge-specific client input. For example:
|
| 119 |
+
``{'credential': password}`` for password challenge.
|
| 120 |
+
access_token (str): Access token with reauth scopes.
|
| 121 |
+
|
| 122 |
+
Returns:
|
| 123 |
+
dict: The response from the reauth API.
|
| 124 |
+
"""
|
| 125 |
+
body = {
|
| 126 |
+
"sessionId": session_id,
|
| 127 |
+
"challengeId": challenge_id,
|
| 128 |
+
"action": "RESPOND",
|
| 129 |
+
"proposalResponse": client_input,
|
| 130 |
+
}
|
| 131 |
+
metrics_header = {metrics.API_CLIENT_HEADER: metrics.reauth_continue()}
|
| 132 |
+
|
| 133 |
+
return _client._token_endpoint_request(
|
| 134 |
+
request,
|
| 135 |
+
_REAUTH_API + "/{}:continue".format(session_id),
|
| 136 |
+
body,
|
| 137 |
+
access_token=access_token,
|
| 138 |
+
use_json=True,
|
| 139 |
+
headers=metrics_header,
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
def _run_next_challenge(msg, request, access_token):
|
| 144 |
+
"""Get the next challenge from msg and run it.
|
| 145 |
+
|
| 146 |
+
Args:
|
| 147 |
+
msg (dict): Reauth API response body (either from the initial request to
|
| 148 |
+
https://reauth.googleapis.com/v2/sessions:start or from sending the
|
| 149 |
+
previous challenge response to
|
| 150 |
+
https://reauth.googleapis.com/v2/sessions/id:continue)
|
| 151 |
+
request (google.auth.transport.Request): A callable used to make
|
| 152 |
+
HTTP requests.
|
| 153 |
+
access_token (str): reauth access token
|
| 154 |
+
|
| 155 |
+
Returns:
|
| 156 |
+
dict: The response from the reauth API.
|
| 157 |
+
|
| 158 |
+
Raises:
|
| 159 |
+
google.auth.exceptions.ReauthError: if reauth failed.
|
| 160 |
+
"""
|
| 161 |
+
for challenge in msg["challenges"]:
|
| 162 |
+
if challenge["status"] != "READY":
|
| 163 |
+
# Skip non-activated challenges.
|
| 164 |
+
continue
|
| 165 |
+
c = challenges.AVAILABLE_CHALLENGES.get(challenge["challengeType"], None)
|
| 166 |
+
if not c:
|
| 167 |
+
raise exceptions.ReauthFailError(
|
| 168 |
+
"Unsupported challenge type {0}. Supported types: {1}".format(
|
| 169 |
+
challenge["challengeType"],
|
| 170 |
+
",".join(list(challenges.AVAILABLE_CHALLENGES.keys())),
|
| 171 |
+
)
|
| 172 |
+
)
|
| 173 |
+
if not c.is_locally_eligible:
|
| 174 |
+
raise exceptions.ReauthFailError(
|
| 175 |
+
"Challenge {0} is not locally eligible".format(
|
| 176 |
+
challenge["challengeType"]
|
| 177 |
+
)
|
| 178 |
+
)
|
| 179 |
+
client_input = c.obtain_challenge_input(challenge)
|
| 180 |
+
if not client_input:
|
| 181 |
+
return None
|
| 182 |
+
return _send_challenge_result(
|
| 183 |
+
request,
|
| 184 |
+
msg["sessionId"],
|
| 185 |
+
challenge["challengeId"],
|
| 186 |
+
client_input,
|
| 187 |
+
access_token,
|
| 188 |
+
)
|
| 189 |
+
return None
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
def _obtain_rapt(request, access_token, requested_scopes):
|
| 193 |
+
"""Given an http request method and reauth access token, get rapt token.
|
| 194 |
+
|
| 195 |
+
Args:
|
| 196 |
+
request (google.auth.transport.Request): A callable used to make
|
| 197 |
+
HTTP requests.
|
| 198 |
+
access_token (str): reauth access token
|
| 199 |
+
requested_scopes (Sequence[str]): scopes required by the client application
|
| 200 |
+
|
| 201 |
+
Returns:
|
| 202 |
+
str: The rapt token.
|
| 203 |
+
|
| 204 |
+
Raises:
|
| 205 |
+
google.auth.exceptions.ReauthError: if reauth failed
|
| 206 |
+
"""
|
| 207 |
+
msg = _get_challenges(
|
| 208 |
+
request,
|
| 209 |
+
list(challenges.AVAILABLE_CHALLENGES.keys()),
|
| 210 |
+
access_token,
|
| 211 |
+
requested_scopes,
|
| 212 |
+
)
|
| 213 |
+
|
| 214 |
+
if msg["status"] == _AUTHENTICATED:
|
| 215 |
+
return msg["encodedProofOfReauthToken"]
|
| 216 |
+
|
| 217 |
+
for _ in range(0, RUN_CHALLENGE_RETRY_LIMIT):
|
| 218 |
+
if not (
|
| 219 |
+
msg["status"] == _CHALLENGE_REQUIRED or msg["status"] == _CHALLENGE_PENDING
|
| 220 |
+
):
|
| 221 |
+
raise exceptions.ReauthFailError(
|
| 222 |
+
"Reauthentication challenge failed due to API error: {}".format(
|
| 223 |
+
msg["status"]
|
| 224 |
+
)
|
| 225 |
+
)
|
| 226 |
+
|
| 227 |
+
if not is_interactive():
|
| 228 |
+
raise exceptions.ReauthFailError(
|
| 229 |
+
"Reauthentication challenge could not be answered because you are not"
|
| 230 |
+
" in an interactive session."
|
| 231 |
+
)
|
| 232 |
+
|
| 233 |
+
msg = _run_next_challenge(msg, request, access_token)
|
| 234 |
+
|
| 235 |
+
if not msg:
|
| 236 |
+
raise exceptions.ReauthFailError("Failed to obtain rapt token.")
|
| 237 |
+
if msg["status"] == _AUTHENTICATED:
|
| 238 |
+
return msg["encodedProofOfReauthToken"]
|
| 239 |
+
|
| 240 |
+
# If we got here it means we didn't get authenticated.
|
| 241 |
+
raise exceptions.ReauthFailError("Failed to obtain rapt token.")
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
def get_rapt_token(
|
| 245 |
+
request, client_id, client_secret, refresh_token, token_uri, scopes=None
|
| 246 |
+
):
|
| 247 |
+
"""Given an http request method and refresh_token, get rapt token.
|
| 248 |
+
|
| 249 |
+
Args:
|
| 250 |
+
request (google.auth.transport.Request): A callable used to make
|
| 251 |
+
HTTP requests.
|
| 252 |
+
client_id (str): client id to get access token for reauth scope.
|
| 253 |
+
client_secret (str): client secret for the client_id
|
| 254 |
+
refresh_token (str): refresh token to refresh access token
|
| 255 |
+
token_uri (str): uri to refresh access token
|
| 256 |
+
scopes (Optional(Sequence[str])): scopes required by the client application
|
| 257 |
+
|
| 258 |
+
Returns:
|
| 259 |
+
str: The rapt token.
|
| 260 |
+
Raises:
|
| 261 |
+
google.auth.exceptions.RefreshError: If reauth failed.
|
| 262 |
+
"""
|
| 263 |
+
sys.stderr.write("Reauthentication required.\n")
|
| 264 |
+
|
| 265 |
+
# Get access token for reauth.
|
| 266 |
+
access_token, _, _, _ = _client.refresh_grant(
|
| 267 |
+
request=request,
|
| 268 |
+
client_id=client_id,
|
| 269 |
+
client_secret=client_secret,
|
| 270 |
+
refresh_token=refresh_token,
|
| 271 |
+
token_uri=token_uri,
|
| 272 |
+
scopes=[_REAUTH_SCOPE],
|
| 273 |
+
)
|
| 274 |
+
|
| 275 |
+
# Get rapt token from reauth API.
|
| 276 |
+
rapt_token = _obtain_rapt(request, access_token, requested_scopes=scopes)
|
| 277 |
+
sys.stderr.write("Reauthentication successful.\n")
|
| 278 |
+
|
| 279 |
+
return rapt_token
|
| 280 |
+
|
| 281 |
+
|
| 282 |
+
def refresh_grant(
|
| 283 |
+
request,
|
| 284 |
+
token_uri,
|
| 285 |
+
refresh_token,
|
| 286 |
+
client_id,
|
| 287 |
+
client_secret,
|
| 288 |
+
scopes=None,
|
| 289 |
+
rapt_token=None,
|
| 290 |
+
enable_reauth_refresh=False,
|
| 291 |
+
):
|
| 292 |
+
"""Implements the reauthentication flow.
|
| 293 |
+
|
| 294 |
+
Args:
|
| 295 |
+
request (google.auth.transport.Request): A callable used to make
|
| 296 |
+
HTTP requests.
|
| 297 |
+
token_uri (str): The OAuth 2.0 authorizations server's token endpoint
|
| 298 |
+
URI.
|
| 299 |
+
refresh_token (str): The refresh token to use to get a new access
|
| 300 |
+
token.
|
| 301 |
+
client_id (str): The OAuth 2.0 application's client ID.
|
| 302 |
+
client_secret (str): The Oauth 2.0 appliaction's client secret.
|
| 303 |
+
scopes (Optional(Sequence[str])): Scopes to request. If present, all
|
| 304 |
+
scopes must be authorized for the refresh token. Useful if refresh
|
| 305 |
+
token has a wild card scope (e.g.
|
| 306 |
+
'https://www.googleapis.com/auth/any-api').
|
| 307 |
+
rapt_token (Optional(str)): The rapt token for reauth.
|
| 308 |
+
enable_reauth_refresh (Optional[bool]): Whether reauth refresh flow
|
| 309 |
+
should be used. The default value is False. This option is for
|
| 310 |
+
gcloud only, other users should use the default value.
|
| 311 |
+
|
| 312 |
+
Returns:
|
| 313 |
+
Tuple[str, Optional[str], Optional[datetime], Mapping[str, str], str]: The
|
| 314 |
+
access token, new refresh token, expiration, the additional data
|
| 315 |
+
returned by the token endpoint, and the rapt token.
|
| 316 |
+
|
| 317 |
+
Raises:
|
| 318 |
+
google.auth.exceptions.RefreshError: If the token endpoint returned
|
| 319 |
+
an error.
|
| 320 |
+
"""
|
| 321 |
+
body = {
|
| 322 |
+
"grant_type": _client._REFRESH_GRANT_TYPE,
|
| 323 |
+
"client_id": client_id,
|
| 324 |
+
"client_secret": client_secret,
|
| 325 |
+
"refresh_token": refresh_token,
|
| 326 |
+
}
|
| 327 |
+
if scopes:
|
| 328 |
+
body["scope"] = " ".join(scopes)
|
| 329 |
+
if rapt_token:
|
| 330 |
+
body["rapt"] = rapt_token
|
| 331 |
+
metrics_header = {metrics.API_CLIENT_HEADER: metrics.token_request_user()}
|
| 332 |
+
|
| 333 |
+
response_status_ok, response_data, retryable_error = _client._token_endpoint_request_no_throw(
|
| 334 |
+
request, token_uri, body, headers=metrics_header
|
| 335 |
+
)
|
| 336 |
+
|
| 337 |
+
if not response_status_ok and isinstance(response_data, str):
|
| 338 |
+
raise exceptions.RefreshError(response_data, retryable=False)
|
| 339 |
+
|
| 340 |
+
if (
|
| 341 |
+
not response_status_ok
|
| 342 |
+
and response_data.get("error") == _REAUTH_NEEDED_ERROR
|
| 343 |
+
and (
|
| 344 |
+
response_data.get("error_subtype") == _REAUTH_NEEDED_ERROR_INVALID_RAPT
|
| 345 |
+
or response_data.get("error_subtype") == _REAUTH_NEEDED_ERROR_RAPT_REQUIRED
|
| 346 |
+
)
|
| 347 |
+
):
|
| 348 |
+
if not enable_reauth_refresh:
|
| 349 |
+
raise exceptions.RefreshError(
|
| 350 |
+
"Reauthentication is needed. Please run `gcloud auth application-default login` to reauthenticate."
|
| 351 |
+
)
|
| 352 |
+
|
| 353 |
+
rapt_token = get_rapt_token(
|
| 354 |
+
request, client_id, client_secret, refresh_token, token_uri, scopes=scopes
|
| 355 |
+
)
|
| 356 |
+
body["rapt"] = rapt_token
|
| 357 |
+
(
|
| 358 |
+
response_status_ok,
|
| 359 |
+
response_data,
|
| 360 |
+
retryable_error,
|
| 361 |
+
) = _client._token_endpoint_request_no_throw(
|
| 362 |
+
request, token_uri, body, headers=metrics_header
|
| 363 |
+
)
|
| 364 |
+
|
| 365 |
+
if not response_status_ok:
|
| 366 |
+
_client._handle_error_response(response_data, retryable_error)
|
| 367 |
+
return _client._handle_refresh_grant_response(response_data, refresh_token) + (
|
| 368 |
+
rapt_token,
|
| 369 |
+
)
|
.venv/lib/python3.11/site-packages/google/oauth2/service_account.py
ADDED
|
@@ -0,0 +1,847 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2016 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""Service Accounts: JSON Web Token (JWT) Profile for OAuth 2.0
|
| 16 |
+
|
| 17 |
+
This module implements the JWT Profile for OAuth 2.0 Authorization Grants
|
| 18 |
+
as defined by `RFC 7523`_ with particular support for how this RFC is
|
| 19 |
+
implemented in Google's infrastructure. Google refers to these credentials
|
| 20 |
+
as *Service Accounts*.
|
| 21 |
+
|
| 22 |
+
Service accounts are used for server-to-server communication, such as
|
| 23 |
+
interactions between a web application server and a Google service. The
|
| 24 |
+
service account belongs to your application instead of to an individual end
|
| 25 |
+
user. In contrast to other OAuth 2.0 profiles, no users are involved and your
|
| 26 |
+
application "acts" as the service account.
|
| 27 |
+
|
| 28 |
+
Typically an application uses a service account when the application uses
|
| 29 |
+
Google APIs to work with its own data rather than a user's data. For example,
|
| 30 |
+
an application that uses Google Cloud Datastore for data persistence would use
|
| 31 |
+
a service account to authenticate its calls to the Google Cloud Datastore API.
|
| 32 |
+
However, an application that needs to access a user's Drive documents would
|
| 33 |
+
use the normal OAuth 2.0 profile.
|
| 34 |
+
|
| 35 |
+
Additionally, Google Apps domain administrators can grant service accounts
|
| 36 |
+
`domain-wide delegation`_ authority to access user data on behalf of users in
|
| 37 |
+
the domain.
|
| 38 |
+
|
| 39 |
+
This profile uses a JWT to acquire an OAuth 2.0 access token. The JWT is used
|
| 40 |
+
in place of the usual authorization token returned during the standard
|
| 41 |
+
OAuth 2.0 Authorization Code grant. The JWT is only used for this purpose, as
|
| 42 |
+
the acquired access token is used as the bearer token when making requests
|
| 43 |
+
using these credentials.
|
| 44 |
+
|
| 45 |
+
This profile differs from normal OAuth 2.0 profile because no user consent
|
| 46 |
+
step is required. The use of the private key allows this profile to assert
|
| 47 |
+
identity directly.
|
| 48 |
+
|
| 49 |
+
This profile also differs from the :mod:`google.auth.jwt` authentication
|
| 50 |
+
because the JWT credentials use the JWT directly as the bearer token. This
|
| 51 |
+
profile instead only uses the JWT to obtain an OAuth 2.0 access token. The
|
| 52 |
+
obtained OAuth 2.0 access token is used as the bearer token.
|
| 53 |
+
|
| 54 |
+
Domain-wide delegation
|
| 55 |
+
----------------------
|
| 56 |
+
|
| 57 |
+
Domain-wide delegation allows a service account to access user data on
|
| 58 |
+
behalf of any user in a Google Apps domain without consent from the user.
|
| 59 |
+
For example, an application that uses the Google Calendar API to add events to
|
| 60 |
+
the calendars of all users in a Google Apps domain would use a service account
|
| 61 |
+
to access the Google Calendar API on behalf of users.
|
| 62 |
+
|
| 63 |
+
The Google Apps administrator must explicitly authorize the service account to
|
| 64 |
+
do this. This authorization step is referred to as "delegating domain-wide
|
| 65 |
+
authority" to a service account.
|
| 66 |
+
|
| 67 |
+
You can use domain-wise delegation by creating a set of credentials with a
|
| 68 |
+
specific subject using :meth:`~Credentials.with_subject`.
|
| 69 |
+
|
| 70 |
+
.. _RFC 7523: https://tools.ietf.org/html/rfc7523
|
| 71 |
+
"""
|
| 72 |
+
|
| 73 |
+
import copy
|
| 74 |
+
import datetime
|
| 75 |
+
|
| 76 |
+
from google.auth import _helpers
|
| 77 |
+
from google.auth import _service_account_info
|
| 78 |
+
from google.auth import credentials
|
| 79 |
+
from google.auth import exceptions
|
| 80 |
+
from google.auth import iam
|
| 81 |
+
from google.auth import jwt
|
| 82 |
+
from google.auth import metrics
|
| 83 |
+
from google.oauth2 import _client
|
| 84 |
+
|
| 85 |
+
_DEFAULT_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
|
| 86 |
+
_GOOGLE_OAUTH2_TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token"
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
class Credentials(
|
| 90 |
+
credentials.Signing,
|
| 91 |
+
credentials.Scoped,
|
| 92 |
+
credentials.CredentialsWithQuotaProject,
|
| 93 |
+
credentials.CredentialsWithTokenUri,
|
| 94 |
+
):
|
| 95 |
+
"""Service account credentials
|
| 96 |
+
|
| 97 |
+
Usually, you'll create these credentials with one of the helper
|
| 98 |
+
constructors. To create credentials using a Google service account
|
| 99 |
+
private key JSON file::
|
| 100 |
+
|
| 101 |
+
credentials = service_account.Credentials.from_service_account_file(
|
| 102 |
+
'service-account.json')
|
| 103 |
+
|
| 104 |
+
Or if you already have the service account file loaded::
|
| 105 |
+
|
| 106 |
+
service_account_info = json.load(open('service_account.json'))
|
| 107 |
+
credentials = service_account.Credentials.from_service_account_info(
|
| 108 |
+
service_account_info)
|
| 109 |
+
|
| 110 |
+
Both helper methods pass on arguments to the constructor, so you can
|
| 111 |
+
specify additional scopes and a subject if necessary::
|
| 112 |
+
|
| 113 |
+
credentials = service_account.Credentials.from_service_account_file(
|
| 114 |
+
'service-account.json',
|
| 115 |
+
scopes=['email'],
|
| 116 |
+
subject='user@example.com')
|
| 117 |
+
|
| 118 |
+
The credentials are considered immutable. If you want to modify the scopes
|
| 119 |
+
or the subject used for delegation, use :meth:`with_scopes` or
|
| 120 |
+
:meth:`with_subject`::
|
| 121 |
+
|
| 122 |
+
scoped_credentials = credentials.with_scopes(['email'])
|
| 123 |
+
delegated_credentials = credentials.with_subject(subject)
|
| 124 |
+
|
| 125 |
+
To add a quota project, use :meth:`with_quota_project`::
|
| 126 |
+
|
| 127 |
+
credentials = credentials.with_quota_project('myproject-123')
|
| 128 |
+
"""
|
| 129 |
+
|
| 130 |
+
def __init__(
|
| 131 |
+
self,
|
| 132 |
+
signer,
|
| 133 |
+
service_account_email,
|
| 134 |
+
token_uri,
|
| 135 |
+
scopes=None,
|
| 136 |
+
default_scopes=None,
|
| 137 |
+
subject=None,
|
| 138 |
+
project_id=None,
|
| 139 |
+
quota_project_id=None,
|
| 140 |
+
additional_claims=None,
|
| 141 |
+
always_use_jwt_access=False,
|
| 142 |
+
universe_domain=credentials.DEFAULT_UNIVERSE_DOMAIN,
|
| 143 |
+
trust_boundary=None,
|
| 144 |
+
):
|
| 145 |
+
"""
|
| 146 |
+
Args:
|
| 147 |
+
signer (google.auth.crypt.Signer): The signer used to sign JWTs.
|
| 148 |
+
service_account_email (str): The service account's email.
|
| 149 |
+
scopes (Sequence[str]): User-defined scopes to request during the
|
| 150 |
+
authorization grant.
|
| 151 |
+
default_scopes (Sequence[str]): Default scopes passed by a
|
| 152 |
+
Google client library. Use 'scopes' for user-defined scopes.
|
| 153 |
+
token_uri (str): The OAuth 2.0 Token URI.
|
| 154 |
+
subject (str): For domain-wide delegation, the email address of the
|
| 155 |
+
user to for which to request delegated access.
|
| 156 |
+
project_id (str): Project ID associated with the service account
|
| 157 |
+
credential.
|
| 158 |
+
quota_project_id (Optional[str]): The project ID used for quota and
|
| 159 |
+
billing.
|
| 160 |
+
additional_claims (Mapping[str, str]): Any additional claims for
|
| 161 |
+
the JWT assertion used in the authorization grant.
|
| 162 |
+
always_use_jwt_access (Optional[bool]): Whether self signed JWT should
|
| 163 |
+
be always used.
|
| 164 |
+
universe_domain (str): The universe domain. The default
|
| 165 |
+
universe domain is googleapis.com. For default value self
|
| 166 |
+
signed jwt is used for token refresh.
|
| 167 |
+
trust_boundary (str): String representation of trust boundary meta.
|
| 168 |
+
|
| 169 |
+
.. note:: Typically one of the helper constructors
|
| 170 |
+
:meth:`from_service_account_file` or
|
| 171 |
+
:meth:`from_service_account_info` are used instead of calling the
|
| 172 |
+
constructor directly.
|
| 173 |
+
"""
|
| 174 |
+
super(Credentials, self).__init__()
|
| 175 |
+
|
| 176 |
+
self._cred_file_path = None
|
| 177 |
+
self._scopes = scopes
|
| 178 |
+
self._default_scopes = default_scopes
|
| 179 |
+
self._signer = signer
|
| 180 |
+
self._service_account_email = service_account_email
|
| 181 |
+
self._subject = subject
|
| 182 |
+
self._project_id = project_id
|
| 183 |
+
self._quota_project_id = quota_project_id
|
| 184 |
+
self._token_uri = token_uri
|
| 185 |
+
self._always_use_jwt_access = always_use_jwt_access
|
| 186 |
+
self._universe_domain = universe_domain or credentials.DEFAULT_UNIVERSE_DOMAIN
|
| 187 |
+
|
| 188 |
+
if universe_domain != credentials.DEFAULT_UNIVERSE_DOMAIN:
|
| 189 |
+
self._always_use_jwt_access = True
|
| 190 |
+
|
| 191 |
+
self._jwt_credentials = None
|
| 192 |
+
|
| 193 |
+
if additional_claims is not None:
|
| 194 |
+
self._additional_claims = additional_claims
|
| 195 |
+
else:
|
| 196 |
+
self._additional_claims = {}
|
| 197 |
+
self._trust_boundary = {"locations": [], "encoded_locations": "0x0"}
|
| 198 |
+
|
| 199 |
+
@classmethod
|
| 200 |
+
def _from_signer_and_info(cls, signer, info, **kwargs):
|
| 201 |
+
"""Creates a Credentials instance from a signer and service account
|
| 202 |
+
info.
|
| 203 |
+
|
| 204 |
+
Args:
|
| 205 |
+
signer (google.auth.crypt.Signer): The signer used to sign JWTs.
|
| 206 |
+
info (Mapping[str, str]): The service account info.
|
| 207 |
+
kwargs: Additional arguments to pass to the constructor.
|
| 208 |
+
|
| 209 |
+
Returns:
|
| 210 |
+
google.auth.jwt.Credentials: The constructed credentials.
|
| 211 |
+
|
| 212 |
+
Raises:
|
| 213 |
+
ValueError: If the info is not in the expected format.
|
| 214 |
+
"""
|
| 215 |
+
return cls(
|
| 216 |
+
signer,
|
| 217 |
+
service_account_email=info["client_email"],
|
| 218 |
+
token_uri=info["token_uri"],
|
| 219 |
+
project_id=info.get("project_id"),
|
| 220 |
+
universe_domain=info.get(
|
| 221 |
+
"universe_domain", credentials.DEFAULT_UNIVERSE_DOMAIN
|
| 222 |
+
),
|
| 223 |
+
trust_boundary=info.get("trust_boundary"),
|
| 224 |
+
**kwargs,
|
| 225 |
+
)
|
| 226 |
+
|
| 227 |
+
@classmethod
|
| 228 |
+
def from_service_account_info(cls, info, **kwargs):
|
| 229 |
+
"""Creates a Credentials instance from parsed service account info.
|
| 230 |
+
|
| 231 |
+
Args:
|
| 232 |
+
info (Mapping[str, str]): The service account info in Google
|
| 233 |
+
format.
|
| 234 |
+
kwargs: Additional arguments to pass to the constructor.
|
| 235 |
+
|
| 236 |
+
Returns:
|
| 237 |
+
google.auth.service_account.Credentials: The constructed
|
| 238 |
+
credentials.
|
| 239 |
+
|
| 240 |
+
Raises:
|
| 241 |
+
ValueError: If the info is not in the expected format.
|
| 242 |
+
"""
|
| 243 |
+
signer = _service_account_info.from_dict(
|
| 244 |
+
info, require=["client_email", "token_uri"]
|
| 245 |
+
)
|
| 246 |
+
return cls._from_signer_and_info(signer, info, **kwargs)
|
| 247 |
+
|
| 248 |
+
@classmethod
|
| 249 |
+
def from_service_account_file(cls, filename, **kwargs):
|
| 250 |
+
"""Creates a Credentials instance from a service account json file.
|
| 251 |
+
|
| 252 |
+
Args:
|
| 253 |
+
filename (str): The path to the service account json file.
|
| 254 |
+
kwargs: Additional arguments to pass to the constructor.
|
| 255 |
+
|
| 256 |
+
Returns:
|
| 257 |
+
google.auth.service_account.Credentials: The constructed
|
| 258 |
+
credentials.
|
| 259 |
+
"""
|
| 260 |
+
info, signer = _service_account_info.from_filename(
|
| 261 |
+
filename, require=["client_email", "token_uri"]
|
| 262 |
+
)
|
| 263 |
+
return cls._from_signer_and_info(signer, info, **kwargs)
|
| 264 |
+
|
| 265 |
+
@property
|
| 266 |
+
def service_account_email(self):
|
| 267 |
+
"""The service account email."""
|
| 268 |
+
return self._service_account_email
|
| 269 |
+
|
| 270 |
+
@property
|
| 271 |
+
def project_id(self):
|
| 272 |
+
"""Project ID associated with this credential."""
|
| 273 |
+
return self._project_id
|
| 274 |
+
|
| 275 |
+
@property
|
| 276 |
+
def requires_scopes(self):
|
| 277 |
+
"""Checks if the credentials requires scopes.
|
| 278 |
+
|
| 279 |
+
Returns:
|
| 280 |
+
bool: True if there are no scopes set otherwise False.
|
| 281 |
+
"""
|
| 282 |
+
return True if not self._scopes else False
|
| 283 |
+
|
| 284 |
+
def _make_copy(self):
|
| 285 |
+
cred = self.__class__(
|
| 286 |
+
self._signer,
|
| 287 |
+
service_account_email=self._service_account_email,
|
| 288 |
+
scopes=copy.copy(self._scopes),
|
| 289 |
+
default_scopes=copy.copy(self._default_scopes),
|
| 290 |
+
token_uri=self._token_uri,
|
| 291 |
+
subject=self._subject,
|
| 292 |
+
project_id=self._project_id,
|
| 293 |
+
quota_project_id=self._quota_project_id,
|
| 294 |
+
additional_claims=self._additional_claims.copy(),
|
| 295 |
+
always_use_jwt_access=self._always_use_jwt_access,
|
| 296 |
+
universe_domain=self._universe_domain,
|
| 297 |
+
)
|
| 298 |
+
cred._cred_file_path = self._cred_file_path
|
| 299 |
+
return cred
|
| 300 |
+
|
| 301 |
+
@_helpers.copy_docstring(credentials.Scoped)
|
| 302 |
+
def with_scopes(self, scopes, default_scopes=None):
|
| 303 |
+
cred = self._make_copy()
|
| 304 |
+
cred._scopes = scopes
|
| 305 |
+
cred._default_scopes = default_scopes
|
| 306 |
+
return cred
|
| 307 |
+
|
| 308 |
+
def with_always_use_jwt_access(self, always_use_jwt_access):
|
| 309 |
+
"""Create a copy of these credentials with the specified always_use_jwt_access value.
|
| 310 |
+
|
| 311 |
+
Args:
|
| 312 |
+
always_use_jwt_access (bool): Whether always use self signed JWT or not.
|
| 313 |
+
|
| 314 |
+
Returns:
|
| 315 |
+
google.auth.service_account.Credentials: A new credentials
|
| 316 |
+
instance.
|
| 317 |
+
Raises:
|
| 318 |
+
google.auth.exceptions.InvalidValue: If the universe domain is not
|
| 319 |
+
default and always_use_jwt_access is False.
|
| 320 |
+
"""
|
| 321 |
+
cred = self._make_copy()
|
| 322 |
+
if (
|
| 323 |
+
cred._universe_domain != credentials.DEFAULT_UNIVERSE_DOMAIN
|
| 324 |
+
and not always_use_jwt_access
|
| 325 |
+
):
|
| 326 |
+
raise exceptions.InvalidValue(
|
| 327 |
+
"always_use_jwt_access should be True for non-default universe domain"
|
| 328 |
+
)
|
| 329 |
+
cred._always_use_jwt_access = always_use_jwt_access
|
| 330 |
+
return cred
|
| 331 |
+
|
| 332 |
+
@_helpers.copy_docstring(credentials.CredentialsWithUniverseDomain)
|
| 333 |
+
def with_universe_domain(self, universe_domain):
|
| 334 |
+
cred = self._make_copy()
|
| 335 |
+
cred._universe_domain = universe_domain
|
| 336 |
+
if universe_domain != credentials.DEFAULT_UNIVERSE_DOMAIN:
|
| 337 |
+
cred._always_use_jwt_access = True
|
| 338 |
+
return cred
|
| 339 |
+
|
| 340 |
+
def with_subject(self, subject):
|
| 341 |
+
"""Create a copy of these credentials with the specified subject.
|
| 342 |
+
|
| 343 |
+
Args:
|
| 344 |
+
subject (str): The subject claim.
|
| 345 |
+
|
| 346 |
+
Returns:
|
| 347 |
+
google.auth.service_account.Credentials: A new credentials
|
| 348 |
+
instance.
|
| 349 |
+
"""
|
| 350 |
+
cred = self._make_copy()
|
| 351 |
+
cred._subject = subject
|
| 352 |
+
return cred
|
| 353 |
+
|
| 354 |
+
def with_claims(self, additional_claims):
|
| 355 |
+
"""Returns a copy of these credentials with modified claims.
|
| 356 |
+
|
| 357 |
+
Args:
|
| 358 |
+
additional_claims (Mapping[str, str]): Any additional claims for
|
| 359 |
+
the JWT payload. This will be merged with the current
|
| 360 |
+
additional claims.
|
| 361 |
+
|
| 362 |
+
Returns:
|
| 363 |
+
google.auth.service_account.Credentials: A new credentials
|
| 364 |
+
instance.
|
| 365 |
+
"""
|
| 366 |
+
new_additional_claims = copy.deepcopy(self._additional_claims)
|
| 367 |
+
new_additional_claims.update(additional_claims or {})
|
| 368 |
+
cred = self._make_copy()
|
| 369 |
+
cred._additional_claims = new_additional_claims
|
| 370 |
+
return cred
|
| 371 |
+
|
| 372 |
+
@_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
|
| 373 |
+
def with_quota_project(self, quota_project_id):
|
| 374 |
+
cred = self._make_copy()
|
| 375 |
+
cred._quota_project_id = quota_project_id
|
| 376 |
+
return cred
|
| 377 |
+
|
| 378 |
+
@_helpers.copy_docstring(credentials.CredentialsWithTokenUri)
|
| 379 |
+
def with_token_uri(self, token_uri):
|
| 380 |
+
cred = self._make_copy()
|
| 381 |
+
cred._token_uri = token_uri
|
| 382 |
+
return cred
|
| 383 |
+
|
| 384 |
+
def _make_authorization_grant_assertion(self):
|
| 385 |
+
"""Create the OAuth 2.0 assertion.
|
| 386 |
+
|
| 387 |
+
This assertion is used during the OAuth 2.0 grant to acquire an
|
| 388 |
+
access token.
|
| 389 |
+
|
| 390 |
+
Returns:
|
| 391 |
+
bytes: The authorization grant assertion.
|
| 392 |
+
"""
|
| 393 |
+
now = _helpers.utcnow()
|
| 394 |
+
lifetime = datetime.timedelta(seconds=_DEFAULT_TOKEN_LIFETIME_SECS)
|
| 395 |
+
expiry = now + lifetime
|
| 396 |
+
|
| 397 |
+
payload = {
|
| 398 |
+
"iat": _helpers.datetime_to_secs(now),
|
| 399 |
+
"exp": _helpers.datetime_to_secs(expiry),
|
| 400 |
+
# The issuer must be the service account email.
|
| 401 |
+
"iss": self._service_account_email,
|
| 402 |
+
# The audience must be the auth token endpoint's URI
|
| 403 |
+
"aud": _GOOGLE_OAUTH2_TOKEN_ENDPOINT,
|
| 404 |
+
"scope": _helpers.scopes_to_string(self._scopes or ()),
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
payload.update(self._additional_claims)
|
| 408 |
+
|
| 409 |
+
# The subject can be a user email for domain-wide delegation.
|
| 410 |
+
if self._subject:
|
| 411 |
+
payload.setdefault("sub", self._subject)
|
| 412 |
+
|
| 413 |
+
token = jwt.encode(self._signer, payload)
|
| 414 |
+
|
| 415 |
+
return token
|
| 416 |
+
|
| 417 |
+
def _use_self_signed_jwt(self):
|
| 418 |
+
# Since domain wide delegation doesn't work with self signed JWT. If
|
| 419 |
+
# subject exists, then we should not use self signed JWT.
|
| 420 |
+
return self._subject is None and self._jwt_credentials is not None
|
| 421 |
+
|
| 422 |
+
def _metric_header_for_usage(self):
|
| 423 |
+
if self._use_self_signed_jwt():
|
| 424 |
+
return metrics.CRED_TYPE_SA_JWT
|
| 425 |
+
return metrics.CRED_TYPE_SA_ASSERTION
|
| 426 |
+
|
| 427 |
+
@_helpers.copy_docstring(credentials.Credentials)
|
| 428 |
+
def refresh(self, request):
|
| 429 |
+
if self._always_use_jwt_access and not self._jwt_credentials:
|
| 430 |
+
# If self signed jwt should be used but jwt credential is not
|
| 431 |
+
# created, try to create one with scopes
|
| 432 |
+
self._create_self_signed_jwt(None)
|
| 433 |
+
|
| 434 |
+
if (
|
| 435 |
+
self._universe_domain != credentials.DEFAULT_UNIVERSE_DOMAIN
|
| 436 |
+
and self._subject
|
| 437 |
+
):
|
| 438 |
+
raise exceptions.RefreshError(
|
| 439 |
+
"domain wide delegation is not supported for non-default universe domain"
|
| 440 |
+
)
|
| 441 |
+
|
| 442 |
+
if self._use_self_signed_jwt():
|
| 443 |
+
self._jwt_credentials.refresh(request)
|
| 444 |
+
self.token = self._jwt_credentials.token.decode()
|
| 445 |
+
self.expiry = self._jwt_credentials.expiry
|
| 446 |
+
else:
|
| 447 |
+
assertion = self._make_authorization_grant_assertion()
|
| 448 |
+
access_token, expiry, _ = _client.jwt_grant(
|
| 449 |
+
request, self._token_uri, assertion
|
| 450 |
+
)
|
| 451 |
+
self.token = access_token
|
| 452 |
+
self.expiry = expiry
|
| 453 |
+
|
| 454 |
+
def _create_self_signed_jwt(self, audience):
|
| 455 |
+
"""Create a self-signed JWT from the credentials if requirements are met.
|
| 456 |
+
|
| 457 |
+
Args:
|
| 458 |
+
audience (str): The service URL. ``https://[API_ENDPOINT]/``
|
| 459 |
+
"""
|
| 460 |
+
# https://google.aip.dev/auth/4111
|
| 461 |
+
if self._always_use_jwt_access:
|
| 462 |
+
if self._scopes:
|
| 463 |
+
additional_claims = {"scope": " ".join(self._scopes)}
|
| 464 |
+
if (
|
| 465 |
+
self._jwt_credentials is None
|
| 466 |
+
or self._jwt_credentials.additional_claims != additional_claims
|
| 467 |
+
):
|
| 468 |
+
self._jwt_credentials = jwt.Credentials.from_signing_credentials(
|
| 469 |
+
self, None, additional_claims=additional_claims
|
| 470 |
+
)
|
| 471 |
+
elif audience:
|
| 472 |
+
if (
|
| 473 |
+
self._jwt_credentials is None
|
| 474 |
+
or self._jwt_credentials._audience != audience
|
| 475 |
+
):
|
| 476 |
+
|
| 477 |
+
self._jwt_credentials = jwt.Credentials.from_signing_credentials(
|
| 478 |
+
self, audience
|
| 479 |
+
)
|
| 480 |
+
elif self._default_scopes:
|
| 481 |
+
additional_claims = {"scope": " ".join(self._default_scopes)}
|
| 482 |
+
if (
|
| 483 |
+
self._jwt_credentials is None
|
| 484 |
+
or additional_claims != self._jwt_credentials.additional_claims
|
| 485 |
+
):
|
| 486 |
+
self._jwt_credentials = jwt.Credentials.from_signing_credentials(
|
| 487 |
+
self, None, additional_claims=additional_claims
|
| 488 |
+
)
|
| 489 |
+
elif not self._scopes and audience:
|
| 490 |
+
self._jwt_credentials = jwt.Credentials.from_signing_credentials(
|
| 491 |
+
self, audience
|
| 492 |
+
)
|
| 493 |
+
|
| 494 |
+
@_helpers.copy_docstring(credentials.Signing)
|
| 495 |
+
def sign_bytes(self, message):
|
| 496 |
+
return self._signer.sign(message)
|
| 497 |
+
|
| 498 |
+
@property # type: ignore
|
| 499 |
+
@_helpers.copy_docstring(credentials.Signing)
|
| 500 |
+
def signer(self):
|
| 501 |
+
return self._signer
|
| 502 |
+
|
| 503 |
+
@property # type: ignore
|
| 504 |
+
@_helpers.copy_docstring(credentials.Signing)
|
| 505 |
+
def signer_email(self):
|
| 506 |
+
return self._service_account_email
|
| 507 |
+
|
| 508 |
+
@_helpers.copy_docstring(credentials.Credentials)
|
| 509 |
+
def get_cred_info(self):
|
| 510 |
+
if self._cred_file_path:
|
| 511 |
+
return {
|
| 512 |
+
"credential_source": self._cred_file_path,
|
| 513 |
+
"credential_type": "service account credentials",
|
| 514 |
+
"principal": self.service_account_email,
|
| 515 |
+
}
|
| 516 |
+
return None
|
| 517 |
+
|
| 518 |
+
|
| 519 |
+
class IDTokenCredentials(
|
| 520 |
+
credentials.Signing,
|
| 521 |
+
credentials.CredentialsWithQuotaProject,
|
| 522 |
+
credentials.CredentialsWithTokenUri,
|
| 523 |
+
):
|
| 524 |
+
"""Open ID Connect ID Token-based service account credentials.
|
| 525 |
+
|
| 526 |
+
These credentials are largely similar to :class:`.Credentials`, but instead
|
| 527 |
+
of using an OAuth 2.0 Access Token as the bearer token, they use an Open
|
| 528 |
+
ID Connect ID Token as the bearer token. These credentials are useful when
|
| 529 |
+
communicating to services that require ID Tokens and can not accept access
|
| 530 |
+
tokens.
|
| 531 |
+
|
| 532 |
+
Usually, you'll create these credentials with one of the helper
|
| 533 |
+
constructors. To create credentials using a Google service account
|
| 534 |
+
private key JSON file::
|
| 535 |
+
|
| 536 |
+
credentials = (
|
| 537 |
+
service_account.IDTokenCredentials.from_service_account_file(
|
| 538 |
+
'service-account.json'))
|
| 539 |
+
|
| 540 |
+
|
| 541 |
+
Or if you already have the service account file loaded::
|
| 542 |
+
|
| 543 |
+
service_account_info = json.load(open('service_account.json'))
|
| 544 |
+
credentials = (
|
| 545 |
+
service_account.IDTokenCredentials.from_service_account_info(
|
| 546 |
+
service_account_info))
|
| 547 |
+
|
| 548 |
+
|
| 549 |
+
Both helper methods pass on arguments to the constructor, so you can
|
| 550 |
+
specify additional scopes and a subject if necessary::
|
| 551 |
+
|
| 552 |
+
credentials = (
|
| 553 |
+
service_account.IDTokenCredentials.from_service_account_file(
|
| 554 |
+
'service-account.json',
|
| 555 |
+
scopes=['email'],
|
| 556 |
+
subject='user@example.com'))
|
| 557 |
+
|
| 558 |
+
|
| 559 |
+
The credentials are considered immutable. If you want to modify the scopes
|
| 560 |
+
or the subject used for delegation, use :meth:`with_scopes` or
|
| 561 |
+
:meth:`with_subject`::
|
| 562 |
+
|
| 563 |
+
scoped_credentials = credentials.with_scopes(['email'])
|
| 564 |
+
delegated_credentials = credentials.with_subject(subject)
|
| 565 |
+
|
| 566 |
+
"""
|
| 567 |
+
|
| 568 |
+
def __init__(
|
| 569 |
+
self,
|
| 570 |
+
signer,
|
| 571 |
+
service_account_email,
|
| 572 |
+
token_uri,
|
| 573 |
+
target_audience,
|
| 574 |
+
additional_claims=None,
|
| 575 |
+
quota_project_id=None,
|
| 576 |
+
universe_domain=credentials.DEFAULT_UNIVERSE_DOMAIN,
|
| 577 |
+
):
|
| 578 |
+
"""
|
| 579 |
+
Args:
|
| 580 |
+
signer (google.auth.crypt.Signer): The signer used to sign JWTs.
|
| 581 |
+
service_account_email (str): The service account's email.
|
| 582 |
+
token_uri (str): The OAuth 2.0 Token URI.
|
| 583 |
+
target_audience (str): The intended audience for these credentials,
|
| 584 |
+
used when requesting the ID Token. The ID Token's ``aud`` claim
|
| 585 |
+
will be set to this string.
|
| 586 |
+
additional_claims (Mapping[str, str]): Any additional claims for
|
| 587 |
+
the JWT assertion used in the authorization grant.
|
| 588 |
+
quota_project_id (Optional[str]): The project ID used for quota and billing.
|
| 589 |
+
universe_domain (str): The universe domain. The default
|
| 590 |
+
universe domain is googleapis.com. For default value IAM ID
|
| 591 |
+
token endponint is used for token refresh. Note that
|
| 592 |
+
iam.serviceAccountTokenCreator role is required to use the IAM
|
| 593 |
+
endpoint.
|
| 594 |
+
.. note:: Typically one of the helper constructors
|
| 595 |
+
:meth:`from_service_account_file` or
|
| 596 |
+
:meth:`from_service_account_info` are used instead of calling the
|
| 597 |
+
constructor directly.
|
| 598 |
+
"""
|
| 599 |
+
super(IDTokenCredentials, self).__init__()
|
| 600 |
+
self._signer = signer
|
| 601 |
+
self._service_account_email = service_account_email
|
| 602 |
+
self._token_uri = token_uri
|
| 603 |
+
self._target_audience = target_audience
|
| 604 |
+
self._quota_project_id = quota_project_id
|
| 605 |
+
self._use_iam_endpoint = False
|
| 606 |
+
|
| 607 |
+
if not universe_domain:
|
| 608 |
+
self._universe_domain = credentials.DEFAULT_UNIVERSE_DOMAIN
|
| 609 |
+
else:
|
| 610 |
+
self._universe_domain = universe_domain
|
| 611 |
+
self._iam_id_token_endpoint = iam._IAM_IDTOKEN_ENDPOINT.replace(
|
| 612 |
+
"googleapis.com", self._universe_domain
|
| 613 |
+
)
|
| 614 |
+
|
| 615 |
+
if self._universe_domain != credentials.DEFAULT_UNIVERSE_DOMAIN:
|
| 616 |
+
self._use_iam_endpoint = True
|
| 617 |
+
|
| 618 |
+
if additional_claims is not None:
|
| 619 |
+
self._additional_claims = additional_claims
|
| 620 |
+
else:
|
| 621 |
+
self._additional_claims = {}
|
| 622 |
+
|
| 623 |
+
@classmethod
|
| 624 |
+
def _from_signer_and_info(cls, signer, info, **kwargs):
|
| 625 |
+
"""Creates a credentials instance from a signer and service account
|
| 626 |
+
info.
|
| 627 |
+
|
| 628 |
+
Args:
|
| 629 |
+
signer (google.auth.crypt.Signer): The signer used to sign JWTs.
|
| 630 |
+
info (Mapping[str, str]): The service account info.
|
| 631 |
+
kwargs: Additional arguments to pass to the constructor.
|
| 632 |
+
|
| 633 |
+
Returns:
|
| 634 |
+
google.auth.jwt.IDTokenCredentials: The constructed credentials.
|
| 635 |
+
|
| 636 |
+
Raises:
|
| 637 |
+
ValueError: If the info is not in the expected format.
|
| 638 |
+
"""
|
| 639 |
+
kwargs.setdefault("service_account_email", info["client_email"])
|
| 640 |
+
kwargs.setdefault("token_uri", info["token_uri"])
|
| 641 |
+
if "universe_domain" in info:
|
| 642 |
+
kwargs["universe_domain"] = info["universe_domain"]
|
| 643 |
+
return cls(signer, **kwargs)
|
| 644 |
+
|
| 645 |
+
@classmethod
|
| 646 |
+
def from_service_account_info(cls, info, **kwargs):
|
| 647 |
+
"""Creates a credentials instance from parsed service account info.
|
| 648 |
+
|
| 649 |
+
Args:
|
| 650 |
+
info (Mapping[str, str]): The service account info in Google
|
| 651 |
+
format.
|
| 652 |
+
kwargs: Additional arguments to pass to the constructor.
|
| 653 |
+
|
| 654 |
+
Returns:
|
| 655 |
+
google.auth.service_account.IDTokenCredentials: The constructed
|
| 656 |
+
credentials.
|
| 657 |
+
|
| 658 |
+
Raises:
|
| 659 |
+
ValueError: If the info is not in the expected format.
|
| 660 |
+
"""
|
| 661 |
+
signer = _service_account_info.from_dict(
|
| 662 |
+
info, require=["client_email", "token_uri"]
|
| 663 |
+
)
|
| 664 |
+
return cls._from_signer_and_info(signer, info, **kwargs)
|
| 665 |
+
|
| 666 |
+
@classmethod
|
| 667 |
+
def from_service_account_file(cls, filename, **kwargs):
|
| 668 |
+
"""Creates a credentials instance from a service account json file.
|
| 669 |
+
|
| 670 |
+
Args:
|
| 671 |
+
filename (str): The path to the service account json file.
|
| 672 |
+
kwargs: Additional arguments to pass to the constructor.
|
| 673 |
+
|
| 674 |
+
Returns:
|
| 675 |
+
google.auth.service_account.IDTokenCredentials: The constructed
|
| 676 |
+
credentials.
|
| 677 |
+
"""
|
| 678 |
+
info, signer = _service_account_info.from_filename(
|
| 679 |
+
filename, require=["client_email", "token_uri"]
|
| 680 |
+
)
|
| 681 |
+
return cls._from_signer_and_info(signer, info, **kwargs)
|
| 682 |
+
|
| 683 |
+
def _make_copy(self):
|
| 684 |
+
cred = self.__class__(
|
| 685 |
+
self._signer,
|
| 686 |
+
service_account_email=self._service_account_email,
|
| 687 |
+
token_uri=self._token_uri,
|
| 688 |
+
target_audience=self._target_audience,
|
| 689 |
+
additional_claims=self._additional_claims.copy(),
|
| 690 |
+
quota_project_id=self.quota_project_id,
|
| 691 |
+
universe_domain=self._universe_domain,
|
| 692 |
+
)
|
| 693 |
+
# _use_iam_endpoint is not exposed in the constructor
|
| 694 |
+
cred._use_iam_endpoint = self._use_iam_endpoint
|
| 695 |
+
return cred
|
| 696 |
+
|
| 697 |
+
def with_target_audience(self, target_audience):
|
| 698 |
+
"""Create a copy of these credentials with the specified target
|
| 699 |
+
audience.
|
| 700 |
+
|
| 701 |
+
Args:
|
| 702 |
+
target_audience (str): The intended audience for these credentials,
|
| 703 |
+
used when requesting the ID Token.
|
| 704 |
+
|
| 705 |
+
Returns:
|
| 706 |
+
google.auth.service_account.IDTokenCredentials: A new credentials
|
| 707 |
+
instance.
|
| 708 |
+
"""
|
| 709 |
+
cred = self._make_copy()
|
| 710 |
+
cred._target_audience = target_audience
|
| 711 |
+
return cred
|
| 712 |
+
|
| 713 |
+
def _with_use_iam_endpoint(self, use_iam_endpoint):
|
| 714 |
+
"""Create a copy of these credentials with the use_iam_endpoint value.
|
| 715 |
+
|
| 716 |
+
Args:
|
| 717 |
+
use_iam_endpoint (bool): If True, IAM generateIdToken endpoint will
|
| 718 |
+
be used instead of the token_uri. Note that
|
| 719 |
+
iam.serviceAccountTokenCreator role is required to use the IAM
|
| 720 |
+
endpoint. The default value is False. This feature is currently
|
| 721 |
+
experimental and subject to change without notice.
|
| 722 |
+
|
| 723 |
+
Returns:
|
| 724 |
+
google.auth.service_account.IDTokenCredentials: A new credentials
|
| 725 |
+
instance.
|
| 726 |
+
Raises:
|
| 727 |
+
google.auth.exceptions.InvalidValue: If the universe domain is not
|
| 728 |
+
default and use_iam_endpoint is False.
|
| 729 |
+
"""
|
| 730 |
+
cred = self._make_copy()
|
| 731 |
+
if (
|
| 732 |
+
cred._universe_domain != credentials.DEFAULT_UNIVERSE_DOMAIN
|
| 733 |
+
and not use_iam_endpoint
|
| 734 |
+
):
|
| 735 |
+
raise exceptions.InvalidValue(
|
| 736 |
+
"use_iam_endpoint should be True for non-default universe domain"
|
| 737 |
+
)
|
| 738 |
+
cred._use_iam_endpoint = use_iam_endpoint
|
| 739 |
+
return cred
|
| 740 |
+
|
| 741 |
+
@_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
|
| 742 |
+
def with_quota_project(self, quota_project_id):
|
| 743 |
+
cred = self._make_copy()
|
| 744 |
+
cred._quota_project_id = quota_project_id
|
| 745 |
+
return cred
|
| 746 |
+
|
| 747 |
+
@_helpers.copy_docstring(credentials.CredentialsWithTokenUri)
|
| 748 |
+
def with_token_uri(self, token_uri):
|
| 749 |
+
cred = self._make_copy()
|
| 750 |
+
cred._token_uri = token_uri
|
| 751 |
+
return cred
|
| 752 |
+
|
| 753 |
+
def _make_authorization_grant_assertion(self):
|
| 754 |
+
"""Create the OAuth 2.0 assertion.
|
| 755 |
+
|
| 756 |
+
This assertion is used during the OAuth 2.0 grant to acquire an
|
| 757 |
+
ID token.
|
| 758 |
+
|
| 759 |
+
Returns:
|
| 760 |
+
bytes: The authorization grant assertion.
|
| 761 |
+
"""
|
| 762 |
+
now = _helpers.utcnow()
|
| 763 |
+
lifetime = datetime.timedelta(seconds=_DEFAULT_TOKEN_LIFETIME_SECS)
|
| 764 |
+
expiry = now + lifetime
|
| 765 |
+
|
| 766 |
+
payload = {
|
| 767 |
+
"iat": _helpers.datetime_to_secs(now),
|
| 768 |
+
"exp": _helpers.datetime_to_secs(expiry),
|
| 769 |
+
# The issuer must be the service account email.
|
| 770 |
+
"iss": self.service_account_email,
|
| 771 |
+
# The audience must be the auth token endpoint's URI
|
| 772 |
+
"aud": _GOOGLE_OAUTH2_TOKEN_ENDPOINT,
|
| 773 |
+
# The target audience specifies which service the ID token is
|
| 774 |
+
# intended for.
|
| 775 |
+
"target_audience": self._target_audience,
|
| 776 |
+
}
|
| 777 |
+
|
| 778 |
+
payload.update(self._additional_claims)
|
| 779 |
+
|
| 780 |
+
token = jwt.encode(self._signer, payload)
|
| 781 |
+
|
| 782 |
+
return token
|
| 783 |
+
|
| 784 |
+
def _refresh_with_iam_endpoint(self, request):
|
| 785 |
+
"""Use IAM generateIdToken endpoint to obtain an ID token.
|
| 786 |
+
|
| 787 |
+
It works as follows:
|
| 788 |
+
|
| 789 |
+
1. First we create a self signed jwt with
|
| 790 |
+
https://www.googleapis.com/auth/iam being the scope.
|
| 791 |
+
|
| 792 |
+
2. Next we use the self signed jwt as the access token, and make a POST
|
| 793 |
+
request to IAM generateIdToken endpoint. The request body is:
|
| 794 |
+
{
|
| 795 |
+
"audience": self._target_audience,
|
| 796 |
+
"includeEmail": "true",
|
| 797 |
+
"useEmailAzp": "true",
|
| 798 |
+
}
|
| 799 |
+
|
| 800 |
+
If the request is succesfully, it will return {"token":"the ID token"},
|
| 801 |
+
and we can extract the ID token and compute its expiry.
|
| 802 |
+
"""
|
| 803 |
+
jwt_credentials = jwt.Credentials.from_signing_credentials(
|
| 804 |
+
self,
|
| 805 |
+
None,
|
| 806 |
+
additional_claims={"scope": "https://www.googleapis.com/auth/iam"},
|
| 807 |
+
)
|
| 808 |
+
jwt_credentials.refresh(request)
|
| 809 |
+
self.token, self.expiry = _client.call_iam_generate_id_token_endpoint(
|
| 810 |
+
request,
|
| 811 |
+
self._iam_id_token_endpoint,
|
| 812 |
+
self.signer_email,
|
| 813 |
+
self._target_audience,
|
| 814 |
+
jwt_credentials.token.decode(),
|
| 815 |
+
self._universe_domain,
|
| 816 |
+
)
|
| 817 |
+
|
| 818 |
+
@_helpers.copy_docstring(credentials.Credentials)
|
| 819 |
+
def refresh(self, request):
|
| 820 |
+
if self._use_iam_endpoint:
|
| 821 |
+
self._refresh_with_iam_endpoint(request)
|
| 822 |
+
else:
|
| 823 |
+
assertion = self._make_authorization_grant_assertion()
|
| 824 |
+
access_token, expiry, _ = _client.id_token_jwt_grant(
|
| 825 |
+
request, self._token_uri, assertion
|
| 826 |
+
)
|
| 827 |
+
self.token = access_token
|
| 828 |
+
self.expiry = expiry
|
| 829 |
+
|
| 830 |
+
@property
|
| 831 |
+
def service_account_email(self):
|
| 832 |
+
"""The service account email."""
|
| 833 |
+
return self._service_account_email
|
| 834 |
+
|
| 835 |
+
@_helpers.copy_docstring(credentials.Signing)
|
| 836 |
+
def sign_bytes(self, message):
|
| 837 |
+
return self._signer.sign(message)
|
| 838 |
+
|
| 839 |
+
@property # type: ignore
|
| 840 |
+
@_helpers.copy_docstring(credentials.Signing)
|
| 841 |
+
def signer(self):
|
| 842 |
+
return self._signer
|
| 843 |
+
|
| 844 |
+
@property # type: ignore
|
| 845 |
+
@_helpers.copy_docstring(credentials.Signing)
|
| 846 |
+
def signer_email(self):
|
| 847 |
+
return self._service_account_email
|
.venv/lib/python3.11/site-packages/google/oauth2/sts.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2020 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""OAuth 2.0 Token Exchange Spec.
|
| 16 |
+
|
| 17 |
+
This module defines a token exchange utility based on the `OAuth 2.0 Token
|
| 18 |
+
Exchange`_ spec. This will be mainly used to exchange external credentials
|
| 19 |
+
for GCP access tokens in workload identity pools to access Google APIs.
|
| 20 |
+
|
| 21 |
+
The implementation will support various types of client authentication as
|
| 22 |
+
allowed in the spec.
|
| 23 |
+
|
| 24 |
+
A deviation on the spec will be for additional Google specific options that
|
| 25 |
+
cannot be easily mapped to parameters defined in the RFC.
|
| 26 |
+
|
| 27 |
+
The returned dictionary response will be based on the `rfc8693 section 2.2.1`_
|
| 28 |
+
spec JSON response.
|
| 29 |
+
|
| 30 |
+
.. _OAuth 2.0 Token Exchange: https://tools.ietf.org/html/rfc8693
|
| 31 |
+
.. _rfc8693 section 2.2.1: https://tools.ietf.org/html/rfc8693#section-2.2.1
|
| 32 |
+
"""
|
| 33 |
+
|
| 34 |
+
import http.client as http_client
|
| 35 |
+
import json
|
| 36 |
+
import urllib
|
| 37 |
+
|
| 38 |
+
from google.oauth2 import utils
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
_URLENCODED_HEADERS = {"Content-Type": "application/x-www-form-urlencoded"}
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class Client(utils.OAuthClientAuthHandler):
|
| 45 |
+
"""Implements the OAuth 2.0 token exchange spec based on
|
| 46 |
+
https://tools.ietf.org/html/rfc8693.
|
| 47 |
+
"""
|
| 48 |
+
|
| 49 |
+
def __init__(self, token_exchange_endpoint, client_authentication=None):
|
| 50 |
+
"""Initializes an STS client instance.
|
| 51 |
+
|
| 52 |
+
Args:
|
| 53 |
+
token_exchange_endpoint (str): The token exchange endpoint.
|
| 54 |
+
client_authentication (Optional(google.oauth2.oauth2_utils.ClientAuthentication)):
|
| 55 |
+
The optional OAuth client authentication credentials if available.
|
| 56 |
+
"""
|
| 57 |
+
super(Client, self).__init__(client_authentication)
|
| 58 |
+
self._token_exchange_endpoint = token_exchange_endpoint
|
| 59 |
+
|
| 60 |
+
def _make_request(self, request, headers, request_body):
|
| 61 |
+
# Initialize request headers.
|
| 62 |
+
request_headers = _URLENCODED_HEADERS.copy()
|
| 63 |
+
|
| 64 |
+
# Inject additional headers.
|
| 65 |
+
if headers:
|
| 66 |
+
for k, v in dict(headers).items():
|
| 67 |
+
request_headers[k] = v
|
| 68 |
+
|
| 69 |
+
# Apply OAuth client authentication.
|
| 70 |
+
self.apply_client_authentication_options(request_headers, request_body)
|
| 71 |
+
|
| 72 |
+
# Execute request.
|
| 73 |
+
response = request(
|
| 74 |
+
url=self._token_exchange_endpoint,
|
| 75 |
+
method="POST",
|
| 76 |
+
headers=request_headers,
|
| 77 |
+
body=urllib.parse.urlencode(request_body).encode("utf-8"),
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
response_body = (
|
| 81 |
+
response.data.decode("utf-8")
|
| 82 |
+
if hasattr(response.data, "decode")
|
| 83 |
+
else response.data
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
# If non-200 response received, translate to OAuthError exception.
|
| 87 |
+
if response.status != http_client.OK:
|
| 88 |
+
utils.handle_error_response(response_body)
|
| 89 |
+
|
| 90 |
+
response_data = json.loads(response_body)
|
| 91 |
+
|
| 92 |
+
# Return successful response.
|
| 93 |
+
return response_data
|
| 94 |
+
|
| 95 |
+
def exchange_token(
|
| 96 |
+
self,
|
| 97 |
+
request,
|
| 98 |
+
grant_type,
|
| 99 |
+
subject_token,
|
| 100 |
+
subject_token_type,
|
| 101 |
+
resource=None,
|
| 102 |
+
audience=None,
|
| 103 |
+
scopes=None,
|
| 104 |
+
requested_token_type=None,
|
| 105 |
+
actor_token=None,
|
| 106 |
+
actor_token_type=None,
|
| 107 |
+
additional_options=None,
|
| 108 |
+
additional_headers=None,
|
| 109 |
+
):
|
| 110 |
+
"""Exchanges the provided token for another type of token based on the
|
| 111 |
+
rfc8693 spec.
|
| 112 |
+
|
| 113 |
+
Args:
|
| 114 |
+
request (google.auth.transport.Request): A callable used to make
|
| 115 |
+
HTTP requests.
|
| 116 |
+
grant_type (str): The OAuth 2.0 token exchange grant type.
|
| 117 |
+
subject_token (str): The OAuth 2.0 token exchange subject token.
|
| 118 |
+
subject_token_type (str): The OAuth 2.0 token exchange subject token type.
|
| 119 |
+
resource (Optional[str]): The optional OAuth 2.0 token exchange resource field.
|
| 120 |
+
audience (Optional[str]): The optional OAuth 2.0 token exchange audience field.
|
| 121 |
+
scopes (Optional[Sequence[str]]): The optional list of scopes to use.
|
| 122 |
+
requested_token_type (Optional[str]): The optional OAuth 2.0 token exchange requested
|
| 123 |
+
token type.
|
| 124 |
+
actor_token (Optional[str]): The optional OAuth 2.0 token exchange actor token.
|
| 125 |
+
actor_token_type (Optional[str]): The optional OAuth 2.0 token exchange actor token type.
|
| 126 |
+
additional_options (Optional[Mapping[str, str]]): The optional additional
|
| 127 |
+
non-standard Google specific options.
|
| 128 |
+
additional_headers (Optional[Mapping[str, str]]): The optional additional
|
| 129 |
+
headers to pass to the token exchange endpoint.
|
| 130 |
+
|
| 131 |
+
Returns:
|
| 132 |
+
Mapping[str, str]: The token exchange JSON-decoded response data containing
|
| 133 |
+
the requested token and its expiration time.
|
| 134 |
+
|
| 135 |
+
Raises:
|
| 136 |
+
google.auth.exceptions.OAuthError: If the token endpoint returned
|
| 137 |
+
an error.
|
| 138 |
+
"""
|
| 139 |
+
# Initialize request body.
|
| 140 |
+
request_body = {
|
| 141 |
+
"grant_type": grant_type,
|
| 142 |
+
"resource": resource,
|
| 143 |
+
"audience": audience,
|
| 144 |
+
"scope": " ".join(scopes or []),
|
| 145 |
+
"requested_token_type": requested_token_type,
|
| 146 |
+
"subject_token": subject_token,
|
| 147 |
+
"subject_token_type": subject_token_type,
|
| 148 |
+
"actor_token": actor_token,
|
| 149 |
+
"actor_token_type": actor_token_type,
|
| 150 |
+
"options": None,
|
| 151 |
+
}
|
| 152 |
+
# Add additional non-standard options.
|
| 153 |
+
if additional_options:
|
| 154 |
+
request_body["options"] = urllib.parse.quote(json.dumps(additional_options))
|
| 155 |
+
# Remove empty fields in request body.
|
| 156 |
+
for k, v in dict(request_body).items():
|
| 157 |
+
if v is None or v == "":
|
| 158 |
+
del request_body[k]
|
| 159 |
+
|
| 160 |
+
return self._make_request(request, additional_headers, request_body)
|
| 161 |
+
|
| 162 |
+
def refresh_token(self, request, refresh_token):
|
| 163 |
+
"""Exchanges a refresh token for an access token based on the
|
| 164 |
+
RFC6749 spec.
|
| 165 |
+
|
| 166 |
+
Args:
|
| 167 |
+
request (google.auth.transport.Request): A callable used to make
|
| 168 |
+
HTTP requests.
|
| 169 |
+
subject_token (str): The OAuth 2.0 refresh token.
|
| 170 |
+
"""
|
| 171 |
+
|
| 172 |
+
return self._make_request(
|
| 173 |
+
request,
|
| 174 |
+
None,
|
| 175 |
+
{"grant_type": "refresh_token", "refresh_token": refresh_token},
|
| 176 |
+
)
|
.venv/lib/python3.11/site-packages/google/oauth2/utils.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2020 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""OAuth 2.0 Utilities.
|
| 16 |
+
|
| 17 |
+
This module provides implementations for various OAuth 2.0 utilities.
|
| 18 |
+
This includes `OAuth error handling`_ and
|
| 19 |
+
`Client authentication for OAuth flows`_.
|
| 20 |
+
|
| 21 |
+
OAuth error handling
|
| 22 |
+
--------------------
|
| 23 |
+
This will define interfaces for handling OAuth related error responses as
|
| 24 |
+
stated in `RFC 6749 section 5.2`_.
|
| 25 |
+
This will include a common function to convert these HTTP error responses to a
|
| 26 |
+
:class:`google.auth.exceptions.OAuthError` exception.
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
Client authentication for OAuth flows
|
| 30 |
+
-------------------------------------
|
| 31 |
+
We introduce an interface for defining client authentication credentials based
|
| 32 |
+
on `RFC 6749 section 2.3.1`_. This will expose the following
|
| 33 |
+
capabilities:
|
| 34 |
+
|
| 35 |
+
* Ability to support basic authentication via request header.
|
| 36 |
+
* Ability to support bearer token authentication via request header.
|
| 37 |
+
* Ability to support client ID / secret authentication via request body.
|
| 38 |
+
|
| 39 |
+
.. _RFC 6749 section 2.3.1: https://tools.ietf.org/html/rfc6749#section-2.3.1
|
| 40 |
+
.. _RFC 6749 section 5.2: https://tools.ietf.org/html/rfc6749#section-5.2
|
| 41 |
+
"""
|
| 42 |
+
|
| 43 |
+
import abc
|
| 44 |
+
import base64
|
| 45 |
+
import enum
|
| 46 |
+
import json
|
| 47 |
+
|
| 48 |
+
from google.auth import exceptions
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
# OAuth client authentication based on
|
| 52 |
+
# https://tools.ietf.org/html/rfc6749#section-2.3.
|
| 53 |
+
class ClientAuthType(enum.Enum):
|
| 54 |
+
basic = 1
|
| 55 |
+
request_body = 2
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
class ClientAuthentication(object):
|
| 59 |
+
"""Defines the client authentication credentials for basic and request-body
|
| 60 |
+
types based on https://tools.ietf.org/html/rfc6749#section-2.3.1.
|
| 61 |
+
"""
|
| 62 |
+
|
| 63 |
+
def __init__(self, client_auth_type, client_id, client_secret=None):
|
| 64 |
+
"""Instantiates a client authentication object containing the client ID
|
| 65 |
+
and secret credentials for basic and response-body auth.
|
| 66 |
+
|
| 67 |
+
Args:
|
| 68 |
+
client_auth_type (google.oauth2.oauth_utils.ClientAuthType): The
|
| 69 |
+
client authentication type.
|
| 70 |
+
client_id (str): The client ID.
|
| 71 |
+
client_secret (Optional[str]): The client secret.
|
| 72 |
+
"""
|
| 73 |
+
self.client_auth_type = client_auth_type
|
| 74 |
+
self.client_id = client_id
|
| 75 |
+
self.client_secret = client_secret
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
class OAuthClientAuthHandler(metaclass=abc.ABCMeta):
|
| 79 |
+
"""Abstract class for handling client authentication in OAuth-based
|
| 80 |
+
operations.
|
| 81 |
+
"""
|
| 82 |
+
|
| 83 |
+
def __init__(self, client_authentication=None):
|
| 84 |
+
"""Instantiates an OAuth client authentication handler.
|
| 85 |
+
|
| 86 |
+
Args:
|
| 87 |
+
client_authentication (Optional[google.oauth2.utils.ClientAuthentication]):
|
| 88 |
+
The OAuth client authentication credentials if available.
|
| 89 |
+
"""
|
| 90 |
+
super(OAuthClientAuthHandler, self).__init__()
|
| 91 |
+
self._client_authentication = client_authentication
|
| 92 |
+
|
| 93 |
+
def apply_client_authentication_options(
|
| 94 |
+
self, headers, request_body=None, bearer_token=None
|
| 95 |
+
):
|
| 96 |
+
"""Applies client authentication on the OAuth request's headers or POST
|
| 97 |
+
body.
|
| 98 |
+
|
| 99 |
+
Args:
|
| 100 |
+
headers (Mapping[str, str]): The HTTP request header.
|
| 101 |
+
request_body (Optional[Mapping[str, str]]): The HTTP request body
|
| 102 |
+
dictionary. For requests that do not support request body, this
|
| 103 |
+
is None and will be ignored.
|
| 104 |
+
bearer_token (Optional[str]): The optional bearer token.
|
| 105 |
+
"""
|
| 106 |
+
# Inject authenticated header.
|
| 107 |
+
self._inject_authenticated_headers(headers, bearer_token)
|
| 108 |
+
# Inject authenticated request body.
|
| 109 |
+
if bearer_token is None:
|
| 110 |
+
self._inject_authenticated_request_body(request_body)
|
| 111 |
+
|
| 112 |
+
def _inject_authenticated_headers(self, headers, bearer_token=None):
|
| 113 |
+
if bearer_token is not None:
|
| 114 |
+
headers["Authorization"] = "Bearer %s" % bearer_token
|
| 115 |
+
elif (
|
| 116 |
+
self._client_authentication is not None
|
| 117 |
+
and self._client_authentication.client_auth_type is ClientAuthType.basic
|
| 118 |
+
):
|
| 119 |
+
username = self._client_authentication.client_id
|
| 120 |
+
password = self._client_authentication.client_secret or ""
|
| 121 |
+
|
| 122 |
+
credentials = base64.b64encode(
|
| 123 |
+
("%s:%s" % (username, password)).encode()
|
| 124 |
+
).decode()
|
| 125 |
+
headers["Authorization"] = "Basic %s" % credentials
|
| 126 |
+
|
| 127 |
+
def _inject_authenticated_request_body(self, request_body):
|
| 128 |
+
if (
|
| 129 |
+
self._client_authentication is not None
|
| 130 |
+
and self._client_authentication.client_auth_type
|
| 131 |
+
is ClientAuthType.request_body
|
| 132 |
+
):
|
| 133 |
+
if request_body is None:
|
| 134 |
+
raise exceptions.OAuthError(
|
| 135 |
+
"HTTP request does not support request-body"
|
| 136 |
+
)
|
| 137 |
+
else:
|
| 138 |
+
request_body["client_id"] = self._client_authentication.client_id
|
| 139 |
+
request_body["client_secret"] = (
|
| 140 |
+
self._client_authentication.client_secret or ""
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def handle_error_response(response_body):
|
| 145 |
+
"""Translates an error response from an OAuth operation into an
|
| 146 |
+
OAuthError exception.
|
| 147 |
+
|
| 148 |
+
Args:
|
| 149 |
+
response_body (str): The decoded response data.
|
| 150 |
+
|
| 151 |
+
Raises:
|
| 152 |
+
google.auth.exceptions.OAuthError
|
| 153 |
+
"""
|
| 154 |
+
try:
|
| 155 |
+
error_components = []
|
| 156 |
+
error_data = json.loads(response_body)
|
| 157 |
+
|
| 158 |
+
error_components.append("Error code {}".format(error_data["error"]))
|
| 159 |
+
if "error_description" in error_data:
|
| 160 |
+
error_components.append(": {}".format(error_data["error_description"]))
|
| 161 |
+
if "error_uri" in error_data:
|
| 162 |
+
error_components.append(" - {}".format(error_data["error_uri"]))
|
| 163 |
+
error_details = "".join(error_components)
|
| 164 |
+
# If no details could be extracted, use the response data.
|
| 165 |
+
except (KeyError, ValueError):
|
| 166 |
+
error_details = response_body
|
| 167 |
+
|
| 168 |
+
raise exceptions.OAuthError(error_details, response_body)
|
.venv/lib/python3.11/site-packages/google/oauth2/webauthn_handler.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import abc
|
| 2 |
+
import os
|
| 3 |
+
import struct
|
| 4 |
+
import subprocess
|
| 5 |
+
|
| 6 |
+
from google.auth import exceptions
|
| 7 |
+
from google.oauth2.webauthn_types import GetRequest, GetResponse
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class WebAuthnHandler(abc.ABC):
|
| 11 |
+
@abc.abstractmethod
|
| 12 |
+
def is_available(self) -> bool:
|
| 13 |
+
"""Check whether this WebAuthn handler is available"""
|
| 14 |
+
raise NotImplementedError("is_available method must be implemented")
|
| 15 |
+
|
| 16 |
+
@abc.abstractmethod
|
| 17 |
+
def get(self, get_request: GetRequest) -> GetResponse:
|
| 18 |
+
"""WebAuthn get (assertion)"""
|
| 19 |
+
raise NotImplementedError("get method must be implemented")
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class PluginHandler(WebAuthnHandler):
|
| 23 |
+
"""Offloads WebAuthn get reqeust to a pluggable command-line tool.
|
| 24 |
+
|
| 25 |
+
Offloads WebAuthn get to a plugin which takes the form of a
|
| 26 |
+
command-line tool. The command-line tool is configurable via the
|
| 27 |
+
PluginHandler._ENV_VAR environment variable.
|
| 28 |
+
|
| 29 |
+
The WebAuthn plugin should implement the following interface:
|
| 30 |
+
|
| 31 |
+
Communication occurs over stdin/stdout, and messages are both sent and
|
| 32 |
+
received in the form:
|
| 33 |
+
|
| 34 |
+
[4 bytes - payload size (little-endian)][variable bytes - json payload]
|
| 35 |
+
"""
|
| 36 |
+
|
| 37 |
+
_ENV_VAR = "GOOGLE_AUTH_WEBAUTHN_PLUGIN"
|
| 38 |
+
|
| 39 |
+
def is_available(self) -> bool:
|
| 40 |
+
try:
|
| 41 |
+
self._find_plugin()
|
| 42 |
+
except Exception:
|
| 43 |
+
return False
|
| 44 |
+
else:
|
| 45 |
+
return True
|
| 46 |
+
|
| 47 |
+
def get(self, get_request: GetRequest) -> GetResponse:
|
| 48 |
+
request_json = get_request.to_json()
|
| 49 |
+
cmd = self._find_plugin()
|
| 50 |
+
response_json = self._call_plugin(cmd, request_json)
|
| 51 |
+
return GetResponse.from_json(response_json)
|
| 52 |
+
|
| 53 |
+
def _call_plugin(self, cmd: str, input_json: str) -> str:
|
| 54 |
+
# Calculate length of input
|
| 55 |
+
input_length = len(input_json)
|
| 56 |
+
length_bytes_le = struct.pack("<I", input_length)
|
| 57 |
+
request = length_bytes_le + input_json.encode()
|
| 58 |
+
|
| 59 |
+
# Call plugin
|
| 60 |
+
process_result = subprocess.run(
|
| 61 |
+
[cmd], input=request, capture_output=True, check=True
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
# Check length of response
|
| 65 |
+
response_len_le = process_result.stdout[:4]
|
| 66 |
+
response_len = struct.unpack("<I", response_len_le)[0]
|
| 67 |
+
response = process_result.stdout[4:]
|
| 68 |
+
if response_len != len(response):
|
| 69 |
+
raise exceptions.MalformedError(
|
| 70 |
+
"Plugin response length {} does not match data {}".format(
|
| 71 |
+
response_len, len(response)
|
| 72 |
+
)
|
| 73 |
+
)
|
| 74 |
+
return response.decode()
|
| 75 |
+
|
| 76 |
+
def _find_plugin(self) -> str:
|
| 77 |
+
plugin_cmd = os.environ.get(PluginHandler._ENV_VAR)
|
| 78 |
+
if plugin_cmd is None:
|
| 79 |
+
raise exceptions.InvalidResource(
|
| 80 |
+
"{} env var is not set".format(PluginHandler._ENV_VAR)
|
| 81 |
+
)
|
| 82 |
+
return plugin_cmd
|
.venv/lib/python3.11/site-packages/google/oauth2/webauthn_types.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dataclasses import dataclass
|
| 2 |
+
import json
|
| 3 |
+
from typing import Any, Dict, List, Optional
|
| 4 |
+
|
| 5 |
+
from google.auth import exceptions
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
@dataclass(frozen=True)
|
| 9 |
+
class PublicKeyCredentialDescriptor:
|
| 10 |
+
"""Descriptor for a security key based credential.
|
| 11 |
+
|
| 12 |
+
https://www.w3.org/TR/webauthn-3/#dictionary-credential-descriptor
|
| 13 |
+
|
| 14 |
+
Args:
|
| 15 |
+
id: <url-safe base64-encoded> credential id (key handle).
|
| 16 |
+
transports: <'usb'|'nfc'|'ble'|'internal'> List of supported transports.
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
id: str
|
| 20 |
+
transports: Optional[List[str]] = None
|
| 21 |
+
|
| 22 |
+
def to_dict(self):
|
| 23 |
+
cred = {"type": "public-key", "id": self.id}
|
| 24 |
+
if self.transports:
|
| 25 |
+
cred["transports"] = self.transports
|
| 26 |
+
return cred
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
@dataclass
|
| 30 |
+
class AuthenticationExtensionsClientInputs:
|
| 31 |
+
"""Client extensions inputs for WebAuthn extensions.
|
| 32 |
+
|
| 33 |
+
Args:
|
| 34 |
+
appid: app id that can be asserted with in addition to rpid.
|
| 35 |
+
https://www.w3.org/TR/webauthn-3/#sctn-appid-extension
|
| 36 |
+
"""
|
| 37 |
+
|
| 38 |
+
appid: Optional[str] = None
|
| 39 |
+
|
| 40 |
+
def to_dict(self):
|
| 41 |
+
extensions = {}
|
| 42 |
+
if self.appid:
|
| 43 |
+
extensions["appid"] = self.appid
|
| 44 |
+
return extensions
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
@dataclass
|
| 48 |
+
class GetRequest:
|
| 49 |
+
"""WebAuthn get request
|
| 50 |
+
|
| 51 |
+
Args:
|
| 52 |
+
origin: Origin where the WebAuthn get assertion takes place.
|
| 53 |
+
rpid: Relying Party ID.
|
| 54 |
+
challenge: <url-safe base64-encoded> raw challenge.
|
| 55 |
+
timeout_ms: Timeout number in millisecond.
|
| 56 |
+
allow_credentials: List of allowed credentials.
|
| 57 |
+
user_verification: <'required'|'preferred'|'discouraged'> User verification requirement.
|
| 58 |
+
extensions: WebAuthn authentication extensions inputs.
|
| 59 |
+
"""
|
| 60 |
+
|
| 61 |
+
origin: str
|
| 62 |
+
rpid: str
|
| 63 |
+
challenge: str
|
| 64 |
+
timeout_ms: Optional[int] = None
|
| 65 |
+
allow_credentials: Optional[List[PublicKeyCredentialDescriptor]] = None
|
| 66 |
+
user_verification: Optional[str] = None
|
| 67 |
+
extensions: Optional[AuthenticationExtensionsClientInputs] = None
|
| 68 |
+
|
| 69 |
+
def to_json(self) -> str:
|
| 70 |
+
req_options: Dict[str, Any] = {"rpid": self.rpid, "challenge": self.challenge}
|
| 71 |
+
if self.timeout_ms:
|
| 72 |
+
req_options["timeout"] = self.timeout_ms
|
| 73 |
+
if self.allow_credentials:
|
| 74 |
+
req_options["allowCredentials"] = [
|
| 75 |
+
c.to_dict() for c in self.allow_credentials
|
| 76 |
+
]
|
| 77 |
+
if self.user_verification:
|
| 78 |
+
req_options["userVerification"] = self.user_verification
|
| 79 |
+
if self.extensions:
|
| 80 |
+
req_options["extensions"] = self.extensions.to_dict()
|
| 81 |
+
return json.dumps(
|
| 82 |
+
{"type": "get", "origin": self.origin, "requestData": req_options}
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
@dataclass(frozen=True)
|
| 87 |
+
class AuthenticatorAssertionResponse:
|
| 88 |
+
"""Authenticator response to a WebAuthn get (assertion) request.
|
| 89 |
+
|
| 90 |
+
https://www.w3.org/TR/webauthn-3/#authenticatorassertionresponse
|
| 91 |
+
|
| 92 |
+
Args:
|
| 93 |
+
client_data_json: <url-safe base64-encoded> client data JSON.
|
| 94 |
+
authenticator_data: <url-safe base64-encoded> authenticator data.
|
| 95 |
+
signature: <url-safe base64-encoded> signature.
|
| 96 |
+
user_handle: <url-safe base64-encoded> user handle.
|
| 97 |
+
"""
|
| 98 |
+
|
| 99 |
+
client_data_json: str
|
| 100 |
+
authenticator_data: str
|
| 101 |
+
signature: str
|
| 102 |
+
user_handle: Optional[str]
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
@dataclass(frozen=True)
|
| 106 |
+
class GetResponse:
|
| 107 |
+
"""WebAuthn get (assertion) response.
|
| 108 |
+
|
| 109 |
+
Args:
|
| 110 |
+
id: <url-safe base64-encoded> credential id (key handle).
|
| 111 |
+
response: The authenticator assertion response.
|
| 112 |
+
authenticator_attachment: <'cross-platform'|'platform'> The attachment status of the authenticator.
|
| 113 |
+
client_extension_results: WebAuthn authentication extensions output results in a dictionary.
|
| 114 |
+
"""
|
| 115 |
+
|
| 116 |
+
id: str
|
| 117 |
+
response: AuthenticatorAssertionResponse
|
| 118 |
+
authenticator_attachment: Optional[str]
|
| 119 |
+
client_extension_results: Optional[Dict]
|
| 120 |
+
|
| 121 |
+
@staticmethod
|
| 122 |
+
def from_json(json_str: str):
|
| 123 |
+
"""Verify and construct GetResponse from a JSON string."""
|
| 124 |
+
try:
|
| 125 |
+
resp_json = json.loads(json_str)
|
| 126 |
+
except ValueError:
|
| 127 |
+
raise exceptions.MalformedError("Invalid Get JSON response")
|
| 128 |
+
if resp_json.get("type") != "getResponse":
|
| 129 |
+
raise exceptions.MalformedError(
|
| 130 |
+
"Invalid Get response type: {}".format(resp_json.get("type"))
|
| 131 |
+
)
|
| 132 |
+
pk_cred = resp_json.get("responseData")
|
| 133 |
+
if pk_cred is None:
|
| 134 |
+
if resp_json.get("error"):
|
| 135 |
+
raise exceptions.ReauthFailError(
|
| 136 |
+
"WebAuthn.get failure: {}".format(resp_json["error"])
|
| 137 |
+
)
|
| 138 |
+
else:
|
| 139 |
+
raise exceptions.MalformedError("Get response is empty")
|
| 140 |
+
if pk_cred.get("type") != "public-key":
|
| 141 |
+
raise exceptions.MalformedError(
|
| 142 |
+
"Invalid credential type: {}".format(pk_cred.get("type"))
|
| 143 |
+
)
|
| 144 |
+
assertion_json = pk_cred["response"]
|
| 145 |
+
assertion_resp = AuthenticatorAssertionResponse(
|
| 146 |
+
client_data_json=assertion_json["clientDataJSON"],
|
| 147 |
+
authenticator_data=assertion_json["authenticatorData"],
|
| 148 |
+
signature=assertion_json["signature"],
|
| 149 |
+
user_handle=assertion_json.get("userHandle"),
|
| 150 |
+
)
|
| 151 |
+
return GetResponse(
|
| 152 |
+
id=pk_cred["id"],
|
| 153 |
+
response=assertion_resp,
|
| 154 |
+
authenticator_attachment=pk_cred.get("authenticatorAttachment"),
|
| 155 |
+
client_extension_results=pk_cred.get("clientExtensionResults"),
|
| 156 |
+
)
|
.venv/lib/python3.11/site-packages/google/type/calendar_period_pb2.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
|
| 3 |
+
# Copyright 2024 Google LLC
|
| 4 |
+
#
|
| 5 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 6 |
+
# you may not use this file except in compliance with the License.
|
| 7 |
+
# You may obtain a copy of the License at
|
| 8 |
+
#
|
| 9 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 10 |
+
#
|
| 11 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 12 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 13 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 14 |
+
# See the License for the specific language governing permissions and
|
| 15 |
+
# limitations under the License.
|
| 16 |
+
|
| 17 |
+
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
| 18 |
+
# source: google/type/calendar_period.proto
|
| 19 |
+
"""Generated protocol buffer code."""
|
| 20 |
+
from google.protobuf import descriptor as _descriptor
|
| 21 |
+
from google.protobuf import descriptor_pool as _descriptor_pool
|
| 22 |
+
from google.protobuf import symbol_database as _symbol_database
|
| 23 |
+
from google.protobuf.internal import builder as _builder
|
| 24 |
+
|
| 25 |
+
# @@protoc_insertion_point(imports)
|
| 26 |
+
|
| 27 |
+
_sym_db = _symbol_database.Default()
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(
|
| 31 |
+
b"\n!google/type/calendar_period.proto\x12\x0bgoogle.type*\x7f\n\x0e\x43\x61lendarPeriod\x12\x1f\n\x1b\x43\x41LENDAR_PERIOD_UNSPECIFIED\x10\x00\x12\x07\n\x03\x44\x41Y\x10\x01\x12\x08\n\x04WEEK\x10\x02\x12\r\n\tFORTNIGHT\x10\x03\x12\t\n\x05MONTH\x10\x04\x12\x0b\n\x07QUARTER\x10\x05\x12\x08\n\x04HALF\x10\x06\x12\x08\n\x04YEAR\x10\x07\x42x\n\x0f\x63om.google.typeB\x13\x43\x61lendarPeriodProtoP\x01ZHgoogle.golang.org/genproto/googleapis/type/calendarperiod;calendarperiod\xa2\x02\x03GTPb\x06proto3"
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
_globals = globals()
|
| 35 |
+
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
| 36 |
+
_builder.BuildTopDescriptorsAndMessages(
|
| 37 |
+
DESCRIPTOR, "google.type.calendar_period_pb2", _globals
|
| 38 |
+
)
|
| 39 |
+
if _descriptor._USE_C_DESCRIPTORS == False:
|
| 40 |
+
DESCRIPTOR._options = None
|
| 41 |
+
DESCRIPTOR._serialized_options = b"\n\017com.google.typeB\023CalendarPeriodProtoP\001ZHgoogle.golang.org/genproto/googleapis/type/calendarperiod;calendarperiod\242\002\003GTP"
|
| 42 |
+
_globals["_CALENDARPERIOD"]._serialized_start = 50
|
| 43 |
+
_globals["_CALENDARPERIOD"]._serialized_end = 177
|
| 44 |
+
# @@protoc_insertion_point(module_scope)
|