structure saas with tools
This commit is contained in:
36
.venv/lib/python3.10/site-packages/google/oauth2/__init__.py
Normal file
36
.venv/lib/python3.10/site-packages/google/oauth2/__init__.py
Normal file
@@ -0,0 +1,36 @@
|
||||
# Copyright 2016 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Google OAuth 2.0 Library for Python."""
|
||||
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
|
||||
class Python37DeprecationWarning(DeprecationWarning): # pragma: NO COVER
|
||||
"""
|
||||
Deprecation warning raised when Python 3.7 runtime is detected.
|
||||
Python 3.7 support will be dropped after January 1, 2024.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
# Checks if the current runtime is Python 3.7.
|
||||
if sys.version_info.major == 3 and sys.version_info.minor == 7: # pragma: NO COVER
|
||||
message = (
|
||||
"After January 1, 2024, new releases of this library will drop support "
|
||||
"for Python 3.7."
|
||||
)
|
||||
warnings.warn(message, Python37DeprecationWarning)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
508
.venv/lib/python3.10/site-packages/google/oauth2/_client.py
Normal file
508
.venv/lib/python3.10/site-packages/google/oauth2/_client.py
Normal file
@@ -0,0 +1,508 @@
|
||||
# Copyright 2016 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""OAuth 2.0 client.
|
||||
|
||||
This is a client for interacting with an OAuth 2.0 authorization server's
|
||||
token endpoint.
|
||||
|
||||
For more information about the token endpoint, see
|
||||
`Section 3.1 of rfc6749`_
|
||||
|
||||
.. _Section 3.1 of rfc6749: https://tools.ietf.org/html/rfc6749#section-3.2
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import http.client as http_client
|
||||
import json
|
||||
import urllib
|
||||
|
||||
from google.auth import _exponential_backoff
|
||||
from google.auth import _helpers
|
||||
from google.auth import credentials
|
||||
from google.auth import exceptions
|
||||
from google.auth import jwt
|
||||
from google.auth import metrics
|
||||
from google.auth import transport
|
||||
|
||||
_URLENCODED_CONTENT_TYPE = "application/x-www-form-urlencoded"
|
||||
_JSON_CONTENT_TYPE = "application/json"
|
||||
_JWT_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer"
|
||||
_REFRESH_GRANT_TYPE = "refresh_token"
|
||||
|
||||
|
||||
def _handle_error_response(response_data, retryable_error):
|
||||
"""Translates an error response into an exception.
|
||||
|
||||
Args:
|
||||
response_data (Mapping | str): The decoded response data.
|
||||
retryable_error Optional[bool]: A boolean indicating if an error is retryable.
|
||||
Defaults to False.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.RefreshError: The errors contained in response_data.
|
||||
"""
|
||||
|
||||
retryable_error = retryable_error if retryable_error else False
|
||||
|
||||
if isinstance(response_data, str):
|
||||
raise exceptions.RefreshError(response_data, retryable=retryable_error)
|
||||
try:
|
||||
error_details = "{}: {}".format(
|
||||
response_data["error"], response_data.get("error_description")
|
||||
)
|
||||
# If no details could be extracted, use the response data.
|
||||
except (KeyError, ValueError):
|
||||
error_details = json.dumps(response_data)
|
||||
|
||||
raise exceptions.RefreshError(
|
||||
error_details, response_data, retryable=retryable_error
|
||||
)
|
||||
|
||||
|
||||
def _can_retry(status_code, response_data):
|
||||
"""Checks if a request can be retried by inspecting the status code
|
||||
and response body of the request.
|
||||
|
||||
Args:
|
||||
status_code (int): The response status code.
|
||||
response_data (Mapping | str): The decoded response data.
|
||||
|
||||
Returns:
|
||||
bool: True if the response is retryable. False otherwise.
|
||||
"""
|
||||
if status_code in transport.DEFAULT_RETRYABLE_STATUS_CODES:
|
||||
return True
|
||||
|
||||
try:
|
||||
# For a failed response, response_body could be a string
|
||||
error_desc = response_data.get("error_description") or ""
|
||||
error_code = response_data.get("error") or ""
|
||||
|
||||
if not isinstance(error_code, str) or not isinstance(error_desc, str):
|
||||
return False
|
||||
|
||||
# Per Oauth 2.0 RFC https://www.rfc-editor.org/rfc/rfc6749.html#section-4.1.2.1
|
||||
# This is needed because a redirect will not return a 500 status code.
|
||||
retryable_error_descriptions = {
|
||||
"internal_failure",
|
||||
"server_error",
|
||||
"temporarily_unavailable",
|
||||
}
|
||||
|
||||
if any(e in retryable_error_descriptions for e in (error_code, error_desc)):
|
||||
return True
|
||||
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _parse_expiry(response_data):
|
||||
"""Parses the expiry field from a response into a datetime.
|
||||
|
||||
Args:
|
||||
response_data (Mapping): The JSON-parsed response data.
|
||||
|
||||
Returns:
|
||||
Optional[datetime]: The expiration or ``None`` if no expiration was
|
||||
specified.
|
||||
"""
|
||||
expires_in = response_data.get("expires_in", None)
|
||||
|
||||
if expires_in is not None:
|
||||
# Some services do not respect the OAUTH2.0 RFC and send expires_in as a
|
||||
# JSON String.
|
||||
if isinstance(expires_in, str):
|
||||
expires_in = int(expires_in)
|
||||
|
||||
return _helpers.utcnow() + datetime.timedelta(seconds=expires_in)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def _token_endpoint_request_no_throw(
|
||||
request,
|
||||
token_uri,
|
||||
body,
|
||||
access_token=None,
|
||||
use_json=False,
|
||||
can_retry=True,
|
||||
headers=None,
|
||||
**kwargs
|
||||
):
|
||||
"""Makes a request to the OAuth 2.0 authorization server's token endpoint.
|
||||
This function doesn't throw on response errors.
|
||||
|
||||
Args:
|
||||
request (google.auth.transport.Request): A callable used to make
|
||||
HTTP requests.
|
||||
token_uri (str): The OAuth 2.0 authorizations server's token endpoint
|
||||
URI.
|
||||
body (Mapping[str, str]): The parameters to send in the request body.
|
||||
access_token (Optional(str)): The access token needed to make the request.
|
||||
use_json (Optional(bool)): Use urlencoded format or json format for the
|
||||
content type. The default value is False.
|
||||
can_retry (bool): Enable or disable request retry behavior.
|
||||
headers (Optional[Mapping[str, str]]): The headers for the request.
|
||||
kwargs: Additional arguments passed on to the request method. The
|
||||
kwargs will be passed to `requests.request` method, see:
|
||||
https://docs.python-requests.org/en/latest/api/#requests.request.
|
||||
For example, you can use `cert=("cert_pem_path", "key_pem_path")`
|
||||
to set up client side SSL certificate, and use
|
||||
`verify="ca_bundle_path"` to set up the CA certificates for sever
|
||||
side SSL certificate verification.
|
||||
|
||||
Returns:
|
||||
Tuple(bool, Mapping[str, str], Optional[bool]): A boolean indicating
|
||||
if the request is successful, a mapping for the JSON-decoded response
|
||||
data and in the case of an error a boolean indicating if the error
|
||||
is retryable.
|
||||
"""
|
||||
if use_json:
|
||||
headers_to_use = {"Content-Type": _JSON_CONTENT_TYPE}
|
||||
body = json.dumps(body).encode("utf-8")
|
||||
else:
|
||||
headers_to_use = {"Content-Type": _URLENCODED_CONTENT_TYPE}
|
||||
body = urllib.parse.urlencode(body).encode("utf-8")
|
||||
|
||||
if access_token:
|
||||
headers_to_use["Authorization"] = "Bearer {}".format(access_token)
|
||||
|
||||
if headers:
|
||||
headers_to_use.update(headers)
|
||||
|
||||
response_data = {}
|
||||
retryable_error = False
|
||||
|
||||
retries = _exponential_backoff.ExponentialBackoff()
|
||||
for _ in retries:
|
||||
response = request(
|
||||
method="POST", url=token_uri, headers=headers_to_use, body=body, **kwargs
|
||||
)
|
||||
response_body = (
|
||||
response.data.decode("utf-8")
|
||||
if hasattr(response.data, "decode")
|
||||
else response.data
|
||||
)
|
||||
|
||||
try:
|
||||
# response_body should be a JSON
|
||||
response_data = json.loads(response_body)
|
||||
except ValueError:
|
||||
response_data = response_body
|
||||
|
||||
if response.status == http_client.OK:
|
||||
return True, response_data, None
|
||||
|
||||
retryable_error = _can_retry(
|
||||
status_code=response.status, response_data=response_data
|
||||
)
|
||||
|
||||
if not can_retry or not retryable_error:
|
||||
return False, response_data, retryable_error
|
||||
|
||||
return False, response_data, retryable_error
|
||||
|
||||
|
||||
def _token_endpoint_request(
|
||||
request,
|
||||
token_uri,
|
||||
body,
|
||||
access_token=None,
|
||||
use_json=False,
|
||||
can_retry=True,
|
||||
headers=None,
|
||||
**kwargs
|
||||
):
|
||||
"""Makes a request to the OAuth 2.0 authorization server's token endpoint.
|
||||
|
||||
Args:
|
||||
request (google.auth.transport.Request): A callable used to make
|
||||
HTTP requests.
|
||||
token_uri (str): The OAuth 2.0 authorizations server's token endpoint
|
||||
URI.
|
||||
body (Mapping[str, str]): The parameters to send in the request body.
|
||||
access_token (Optional(str)): The access token needed to make the request.
|
||||
use_json (Optional(bool)): Use urlencoded format or json format for the
|
||||
content type. The default value is False.
|
||||
can_retry (bool): Enable or disable request retry behavior.
|
||||
headers (Optional[Mapping[str, str]]): The headers for the request.
|
||||
kwargs: Additional arguments passed on to the request method. The
|
||||
kwargs will be passed to `requests.request` method, see:
|
||||
https://docs.python-requests.org/en/latest/api/#requests.request.
|
||||
For example, you can use `cert=("cert_pem_path", "key_pem_path")`
|
||||
to set up client side SSL certificate, and use
|
||||
`verify="ca_bundle_path"` to set up the CA certificates for sever
|
||||
side SSL certificate verification.
|
||||
|
||||
Returns:
|
||||
Mapping[str, str]: The JSON-decoded response data.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.RefreshError: If the token endpoint returned
|
||||
an error.
|
||||
"""
|
||||
|
||||
response_status_ok, response_data, retryable_error = _token_endpoint_request_no_throw(
|
||||
request,
|
||||
token_uri,
|
||||
body,
|
||||
access_token=access_token,
|
||||
use_json=use_json,
|
||||
can_retry=can_retry,
|
||||
headers=headers,
|
||||
**kwargs
|
||||
)
|
||||
if not response_status_ok:
|
||||
_handle_error_response(response_data, retryable_error)
|
||||
return response_data
|
||||
|
||||
|
||||
def jwt_grant(request, token_uri, assertion, can_retry=True):
|
||||
"""Implements the JWT Profile for OAuth 2.0 Authorization Grants.
|
||||
|
||||
For more details, see `rfc7523 section 4`_.
|
||||
|
||||
Args:
|
||||
request (google.auth.transport.Request): A callable used to make
|
||||
HTTP requests.
|
||||
token_uri (str): The OAuth 2.0 authorizations server's token endpoint
|
||||
URI.
|
||||
assertion (str): The OAuth 2.0 assertion.
|
||||
can_retry (bool): Enable or disable request retry behavior.
|
||||
|
||||
Returns:
|
||||
Tuple[str, Optional[datetime], Mapping[str, str]]: The access token,
|
||||
expiration, and additional data returned by the token endpoint.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.RefreshError: If the token endpoint returned
|
||||
an error.
|
||||
|
||||
.. _rfc7523 section 4: https://tools.ietf.org/html/rfc7523#section-4
|
||||
"""
|
||||
body = {"assertion": assertion, "grant_type": _JWT_GRANT_TYPE}
|
||||
|
||||
response_data = _token_endpoint_request(
|
||||
request,
|
||||
token_uri,
|
||||
body,
|
||||
can_retry=can_retry,
|
||||
headers={
|
||||
metrics.API_CLIENT_HEADER: metrics.token_request_access_token_sa_assertion()
|
||||
},
|
||||
)
|
||||
|
||||
try:
|
||||
access_token = response_data["access_token"]
|
||||
except KeyError as caught_exc:
|
||||
new_exc = exceptions.RefreshError(
|
||||
"No access token in response.", response_data, retryable=False
|
||||
)
|
||||
raise new_exc from caught_exc
|
||||
|
||||
expiry = _parse_expiry(response_data)
|
||||
|
||||
return access_token, expiry, response_data
|
||||
|
||||
|
||||
def call_iam_generate_id_token_endpoint(
|
||||
request,
|
||||
iam_id_token_endpoint,
|
||||
signer_email,
|
||||
audience,
|
||||
access_token,
|
||||
universe_domain=credentials.DEFAULT_UNIVERSE_DOMAIN,
|
||||
):
|
||||
"""Call iam.generateIdToken endpoint to get ID token.
|
||||
|
||||
Args:
|
||||
request (google.auth.transport.Request): A callable used to make
|
||||
HTTP requests.
|
||||
iam_id_token_endpoint (str): The IAM ID token endpoint to use.
|
||||
signer_email (str): The signer email used to form the IAM
|
||||
generateIdToken endpoint.
|
||||
audience (str): The audience for the ID token.
|
||||
access_token (str): The access token used to call the IAM endpoint.
|
||||
|
||||
Returns:
|
||||
Tuple[str, datetime]: The ID token and expiration.
|
||||
"""
|
||||
body = {"audience": audience, "includeEmail": "true", "useEmailAzp": "true"}
|
||||
|
||||
response_data = _token_endpoint_request(
|
||||
request,
|
||||
iam_id_token_endpoint.replace(
|
||||
credentials.DEFAULT_UNIVERSE_DOMAIN, universe_domain
|
||||
).format(signer_email),
|
||||
body,
|
||||
access_token=access_token,
|
||||
use_json=True,
|
||||
)
|
||||
|
||||
try:
|
||||
id_token = response_data["token"]
|
||||
except KeyError as caught_exc:
|
||||
new_exc = exceptions.RefreshError(
|
||||
"No ID token in response.", response_data, retryable=False
|
||||
)
|
||||
raise new_exc from caught_exc
|
||||
|
||||
payload = jwt.decode(id_token, verify=False)
|
||||
expiry = datetime.datetime.utcfromtimestamp(payload["exp"])
|
||||
|
||||
return id_token, expiry
|
||||
|
||||
|
||||
def id_token_jwt_grant(request, token_uri, assertion, can_retry=True):
|
||||
"""Implements the JWT Profile for OAuth 2.0 Authorization Grants, but
|
||||
requests an OpenID Connect ID Token instead of an access token.
|
||||
|
||||
This is a variant on the standard JWT Profile that is currently unique
|
||||
to Google. This was added for the benefit of authenticating to services
|
||||
that require ID Tokens instead of access tokens or JWT bearer tokens.
|
||||
|
||||
Args:
|
||||
request (google.auth.transport.Request): A callable used to make
|
||||
HTTP requests.
|
||||
token_uri (str): The OAuth 2.0 authorization server's token endpoint
|
||||
URI.
|
||||
assertion (str): JWT token signed by a service account. The token's
|
||||
payload must include a ``target_audience`` claim.
|
||||
can_retry (bool): Enable or disable request retry behavior.
|
||||
|
||||
Returns:
|
||||
Tuple[str, Optional[datetime], Mapping[str, str]]:
|
||||
The (encoded) Open ID Connect ID Token, expiration, and additional
|
||||
data returned by the endpoint.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.RefreshError: If the token endpoint returned
|
||||
an error.
|
||||
"""
|
||||
body = {"assertion": assertion, "grant_type": _JWT_GRANT_TYPE}
|
||||
|
||||
response_data = _token_endpoint_request(
|
||||
request,
|
||||
token_uri,
|
||||
body,
|
||||
can_retry=can_retry,
|
||||
headers={
|
||||
metrics.API_CLIENT_HEADER: metrics.token_request_id_token_sa_assertion()
|
||||
},
|
||||
)
|
||||
|
||||
try:
|
||||
id_token = response_data["id_token"]
|
||||
except KeyError as caught_exc:
|
||||
new_exc = exceptions.RefreshError(
|
||||
"No ID token in response.", response_data, retryable=False
|
||||
)
|
||||
raise new_exc from caught_exc
|
||||
|
||||
payload = jwt.decode(id_token, verify=False)
|
||||
expiry = datetime.datetime.utcfromtimestamp(payload["exp"])
|
||||
|
||||
return id_token, expiry, response_data
|
||||
|
||||
|
||||
def _handle_refresh_grant_response(response_data, refresh_token):
|
||||
"""Extract tokens from refresh grant response.
|
||||
|
||||
Args:
|
||||
response_data (Mapping[str, str]): Refresh grant response data.
|
||||
refresh_token (str): Current refresh token.
|
||||
|
||||
Returns:
|
||||
Tuple[str, str, Optional[datetime], Mapping[str, str]]: The access token,
|
||||
refresh token, expiration, and additional data returned by the token
|
||||
endpoint. If response_data doesn't have refresh token, then the current
|
||||
refresh token will be returned.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.RefreshError: If the token endpoint returned
|
||||
an error.
|
||||
"""
|
||||
try:
|
||||
access_token = response_data["access_token"]
|
||||
except KeyError as caught_exc:
|
||||
new_exc = exceptions.RefreshError(
|
||||
"No access token in response.", response_data, retryable=False
|
||||
)
|
||||
raise new_exc from caught_exc
|
||||
|
||||
refresh_token = response_data.get("refresh_token", refresh_token)
|
||||
expiry = _parse_expiry(response_data)
|
||||
|
||||
return access_token, refresh_token, expiry, response_data
|
||||
|
||||
|
||||
def refresh_grant(
|
||||
request,
|
||||
token_uri,
|
||||
refresh_token,
|
||||
client_id,
|
||||
client_secret,
|
||||
scopes=None,
|
||||
rapt_token=None,
|
||||
can_retry=True,
|
||||
):
|
||||
"""Implements the OAuth 2.0 refresh token grant.
|
||||
|
||||
For more details, see `rfc678 section 6`_.
|
||||
|
||||
Args:
|
||||
request (google.auth.transport.Request): A callable used to make
|
||||
HTTP requests.
|
||||
token_uri (str): The OAuth 2.0 authorizations server's token endpoint
|
||||
URI.
|
||||
refresh_token (str): The refresh token to use to get a new access
|
||||
token.
|
||||
client_id (str): The OAuth 2.0 application's client ID.
|
||||
client_secret (str): The Oauth 2.0 appliaction's client secret.
|
||||
scopes (Optional(Sequence[str])): Scopes to request. If present, all
|
||||
scopes must be authorized for the refresh token. Useful if refresh
|
||||
token has a wild card scope (e.g.
|
||||
'https://www.googleapis.com/auth/any-api').
|
||||
rapt_token (Optional(str)): The reauth Proof Token.
|
||||
can_retry (bool): Enable or disable request retry behavior.
|
||||
|
||||
Returns:
|
||||
Tuple[str, str, Optional[datetime], Mapping[str, str]]: The access
|
||||
token, new or current refresh token, expiration, and additional data
|
||||
returned by the token endpoint.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.RefreshError: If the token endpoint returned
|
||||
an error.
|
||||
|
||||
.. _rfc6748 section 6: https://tools.ietf.org/html/rfc6749#section-6
|
||||
"""
|
||||
body = {
|
||||
"grant_type": _REFRESH_GRANT_TYPE,
|
||||
"client_id": client_id,
|
||||
"client_secret": client_secret,
|
||||
"refresh_token": refresh_token,
|
||||
}
|
||||
if scopes:
|
||||
body["scope"] = " ".join(scopes)
|
||||
if rapt_token:
|
||||
body["rapt"] = rapt_token
|
||||
|
||||
response_data = _token_endpoint_request(
|
||||
request, token_uri, body, can_retry=can_retry
|
||||
)
|
||||
return _handle_refresh_grant_response(response_data, refresh_token)
|
||||
@@ -0,0 +1,286 @@
|
||||
# Copyright 2020 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""OAuth 2.0 async client.
|
||||
|
||||
This is a client for interacting with an OAuth 2.0 authorization server's
|
||||
token endpoint.
|
||||
|
||||
For more information about the token endpoint, see
|
||||
`Section 3.1 of rfc6749`_
|
||||
|
||||
.. _Section 3.1 of rfc6749: https://tools.ietf.org/html/rfc6749#section-3.2
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import http.client as http_client
|
||||
import json
|
||||
import urllib
|
||||
|
||||
from google.auth import _exponential_backoff
|
||||
from google.auth import exceptions
|
||||
from google.auth import jwt
|
||||
from google.oauth2 import _client as client
|
||||
|
||||
|
||||
async def _token_endpoint_request_no_throw(
|
||||
request, token_uri, body, access_token=None, use_json=False, can_retry=True
|
||||
):
|
||||
"""Makes a request to the OAuth 2.0 authorization server's token endpoint.
|
||||
This function doesn't throw on response errors.
|
||||
|
||||
Args:
|
||||
request (google.auth.transport.Request): A callable used to make
|
||||
HTTP requests.
|
||||
token_uri (str): The OAuth 2.0 authorizations server's token endpoint
|
||||
URI.
|
||||
body (Mapping[str, str]): The parameters to send in the request body.
|
||||
access_token (Optional(str)): The access token needed to make the request.
|
||||
use_json (Optional(bool)): Use urlencoded format or json format for the
|
||||
content type. The default value is False.
|
||||
can_retry (bool): Enable or disable request retry behavior.
|
||||
|
||||
Returns:
|
||||
Tuple(bool, Mapping[str, str], Optional[bool]): A boolean indicating
|
||||
if the request is successful, a mapping for the JSON-decoded response
|
||||
data and in the case of an error a boolean indicating if the error
|
||||
is retryable.
|
||||
"""
|
||||
if use_json:
|
||||
headers = {"Content-Type": client._JSON_CONTENT_TYPE}
|
||||
body = json.dumps(body).encode("utf-8")
|
||||
else:
|
||||
headers = {"Content-Type": client._URLENCODED_CONTENT_TYPE}
|
||||
body = urllib.parse.urlencode(body).encode("utf-8")
|
||||
|
||||
if access_token:
|
||||
headers["Authorization"] = "Bearer {}".format(access_token)
|
||||
|
||||
response_data = {}
|
||||
retryable_error = False
|
||||
|
||||
retries = _exponential_backoff.ExponentialBackoff()
|
||||
for _ in retries:
|
||||
response = await request(
|
||||
method="POST", url=token_uri, headers=headers, body=body
|
||||
)
|
||||
|
||||
# Using data.read() resulted in zlib decompression errors. This may require future investigation.
|
||||
response_body1 = await response.content()
|
||||
|
||||
response_body = (
|
||||
response_body1.decode("utf-8")
|
||||
if hasattr(response_body1, "decode")
|
||||
else response_body1
|
||||
)
|
||||
|
||||
try:
|
||||
response_data = json.loads(response_body)
|
||||
except ValueError:
|
||||
response_data = response_body
|
||||
|
||||
if response.status == http_client.OK:
|
||||
return True, response_data, None
|
||||
|
||||
retryable_error = client._can_retry(
|
||||
status_code=response.status, response_data=response_data
|
||||
)
|
||||
|
||||
if not can_retry or not retryable_error:
|
||||
return False, response_data, retryable_error
|
||||
|
||||
return False, response_data, retryable_error
|
||||
|
||||
|
||||
async def _token_endpoint_request(
|
||||
request, token_uri, body, access_token=None, use_json=False, can_retry=True
|
||||
):
|
||||
"""Makes a request to the OAuth 2.0 authorization server's token endpoint.
|
||||
|
||||
Args:
|
||||
request (google.auth.transport.Request): A callable used to make
|
||||
HTTP requests.
|
||||
token_uri (str): The OAuth 2.0 authorizations server's token endpoint
|
||||
URI.
|
||||
body (Mapping[str, str]): The parameters to send in the request body.
|
||||
access_token (Optional(str)): The access token needed to make the request.
|
||||
use_json (Optional(bool)): Use urlencoded format or json format for the
|
||||
content type. The default value is False.
|
||||
can_retry (bool): Enable or disable request retry behavior.
|
||||
|
||||
Returns:
|
||||
Mapping[str, str]: The JSON-decoded response data.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.RefreshError: If the token endpoint returned
|
||||
an error.
|
||||
"""
|
||||
|
||||
response_status_ok, response_data, retryable_error = await _token_endpoint_request_no_throw(
|
||||
request,
|
||||
token_uri,
|
||||
body,
|
||||
access_token=access_token,
|
||||
use_json=use_json,
|
||||
can_retry=can_retry,
|
||||
)
|
||||
if not response_status_ok:
|
||||
client._handle_error_response(response_data, retryable_error)
|
||||
return response_data
|
||||
|
||||
|
||||
async def jwt_grant(request, token_uri, assertion, can_retry=True):
|
||||
"""Implements the JWT Profile for OAuth 2.0 Authorization Grants.
|
||||
|
||||
For more details, see `rfc7523 section 4`_.
|
||||
|
||||
Args:
|
||||
request (google.auth.transport.Request): A callable used to make
|
||||
HTTP requests.
|
||||
token_uri (str): The OAuth 2.0 authorizations server's token endpoint
|
||||
URI.
|
||||
assertion (str): The OAuth 2.0 assertion.
|
||||
can_retry (bool): Enable or disable request retry behavior.
|
||||
|
||||
Returns:
|
||||
Tuple[str, Optional[datetime], Mapping[str, str]]: The access token,
|
||||
expiration, and additional data returned by the token endpoint.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.RefreshError: If the token endpoint returned
|
||||
an error.
|
||||
|
||||
.. _rfc7523 section 4: https://tools.ietf.org/html/rfc7523#section-4
|
||||
"""
|
||||
body = {"assertion": assertion, "grant_type": client._JWT_GRANT_TYPE}
|
||||
|
||||
response_data = await _token_endpoint_request(
|
||||
request, token_uri, body, can_retry=can_retry
|
||||
)
|
||||
|
||||
try:
|
||||
access_token = response_data["access_token"]
|
||||
except KeyError as caught_exc:
|
||||
new_exc = exceptions.RefreshError(
|
||||
"No access token in response.", response_data, retryable=False
|
||||
)
|
||||
raise new_exc from caught_exc
|
||||
|
||||
expiry = client._parse_expiry(response_data)
|
||||
|
||||
return access_token, expiry, response_data
|
||||
|
||||
|
||||
async def id_token_jwt_grant(request, token_uri, assertion, can_retry=True):
|
||||
"""Implements the JWT Profile for OAuth 2.0 Authorization Grants, but
|
||||
requests an OpenID Connect ID Token instead of an access token.
|
||||
|
||||
This is a variant on the standard JWT Profile that is currently unique
|
||||
to Google. This was added for the benefit of authenticating to services
|
||||
that require ID Tokens instead of access tokens or JWT bearer tokens.
|
||||
|
||||
Args:
|
||||
request (google.auth.transport.Request): A callable used to make
|
||||
HTTP requests.
|
||||
token_uri (str): The OAuth 2.0 authorization server's token endpoint
|
||||
URI.
|
||||
assertion (str): JWT token signed by a service account. The token's
|
||||
payload must include a ``target_audience`` claim.
|
||||
can_retry (bool): Enable or disable request retry behavior.
|
||||
|
||||
Returns:
|
||||
Tuple[str, Optional[datetime], Mapping[str, str]]:
|
||||
The (encoded) Open ID Connect ID Token, expiration, and additional
|
||||
data returned by the endpoint.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.RefreshError: If the token endpoint returned
|
||||
an error.
|
||||
"""
|
||||
body = {"assertion": assertion, "grant_type": client._JWT_GRANT_TYPE}
|
||||
|
||||
response_data = await _token_endpoint_request(
|
||||
request, token_uri, body, can_retry=can_retry
|
||||
)
|
||||
|
||||
try:
|
||||
id_token = response_data["id_token"]
|
||||
except KeyError as caught_exc:
|
||||
new_exc = exceptions.RefreshError(
|
||||
"No ID token in response.", response_data, retryable=False
|
||||
)
|
||||
raise new_exc from caught_exc
|
||||
|
||||
payload = jwt.decode(id_token, verify=False)
|
||||
expiry = datetime.datetime.utcfromtimestamp(payload["exp"])
|
||||
|
||||
return id_token, expiry, response_data
|
||||
|
||||
|
||||
async def refresh_grant(
|
||||
request,
|
||||
token_uri,
|
||||
refresh_token,
|
||||
client_id,
|
||||
client_secret,
|
||||
scopes=None,
|
||||
rapt_token=None,
|
||||
can_retry=True,
|
||||
):
|
||||
"""Implements the OAuth 2.0 refresh token grant.
|
||||
|
||||
For more details, see `rfc678 section 6`_.
|
||||
|
||||
Args:
|
||||
request (google.auth.transport.Request): A callable used to make
|
||||
HTTP requests.
|
||||
token_uri (str): The OAuth 2.0 authorizations server's token endpoint
|
||||
URI.
|
||||
refresh_token (str): The refresh token to use to get a new access
|
||||
token.
|
||||
client_id (str): The OAuth 2.0 application's client ID.
|
||||
client_secret (str): The Oauth 2.0 appliaction's client secret.
|
||||
scopes (Optional(Sequence[str])): Scopes to request. If present, all
|
||||
scopes must be authorized for the refresh token. Useful if refresh
|
||||
token has a wild card scope (e.g.
|
||||
'https://www.googleapis.com/auth/any-api').
|
||||
rapt_token (Optional(str)): The reauth Proof Token.
|
||||
can_retry (bool): Enable or disable request retry behavior.
|
||||
|
||||
Returns:
|
||||
Tuple[str, Optional[str], Optional[datetime], Mapping[str, str]]: The
|
||||
access token, new or current refresh token, expiration, and additional data
|
||||
returned by the token endpoint.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.RefreshError: If the token endpoint returned
|
||||
an error.
|
||||
|
||||
.. _rfc6748 section 6: https://tools.ietf.org/html/rfc6749#section-6
|
||||
"""
|
||||
body = {
|
||||
"grant_type": client._REFRESH_GRANT_TYPE,
|
||||
"client_id": client_id,
|
||||
"client_secret": client_secret,
|
||||
"refresh_token": refresh_token,
|
||||
}
|
||||
if scopes:
|
||||
body["scope"] = " ".join(scopes)
|
||||
if rapt_token:
|
||||
body["rapt"] = rapt_token
|
||||
|
||||
response_data = await _token_endpoint_request(
|
||||
request, token_uri, body, can_retry=can_retry
|
||||
)
|
||||
return client._handle_refresh_grant_response(response_data, refresh_token)
|
||||
@@ -0,0 +1,118 @@
|
||||
# Copyright 2020 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""OAuth 2.0 Async Credentials.
|
||||
|
||||
This module provides credentials based on OAuth 2.0 access and refresh tokens.
|
||||
These credentials usually access resources on behalf of a user (resource
|
||||
owner).
|
||||
|
||||
Specifically, this is intended to use access tokens acquired using the
|
||||
`Authorization Code grant`_ and can refresh those tokens using a
|
||||
optional `refresh token`_.
|
||||
|
||||
Obtaining the initial access and refresh token is outside of the scope of this
|
||||
module. Consult `rfc6749 section 4.1`_ for complete details on the
|
||||
Authorization Code grant flow.
|
||||
|
||||
.. _Authorization Code grant: https://tools.ietf.org/html/rfc6749#section-1.3.1
|
||||
.. _refresh token: https://tools.ietf.org/html/rfc6749#section-6
|
||||
.. _rfc6749 section 4.1: https://tools.ietf.org/html/rfc6749#section-4.1
|
||||
"""
|
||||
|
||||
from google.auth import _credentials_async as credentials
|
||||
from google.auth import _helpers
|
||||
from google.auth import exceptions
|
||||
from google.oauth2 import _reauth_async as reauth
|
||||
from google.oauth2 import credentials as oauth2_credentials
|
||||
|
||||
|
||||
class Credentials(oauth2_credentials.Credentials):
|
||||
"""Credentials using OAuth 2.0 access and refresh tokens.
|
||||
|
||||
The credentials are considered immutable. If you want to modify the
|
||||
quota project, use :meth:`with_quota_project` or ::
|
||||
|
||||
credentials = credentials.with_quota_project('myproject-123)
|
||||
"""
|
||||
|
||||
@_helpers.copy_docstring(credentials.Credentials)
|
||||
async def refresh(self, request):
|
||||
if (
|
||||
self._refresh_token is None
|
||||
or self._token_uri is None
|
||||
or self._client_id is None
|
||||
or self._client_secret is None
|
||||
):
|
||||
raise exceptions.RefreshError(
|
||||
"The credentials do not contain the necessary fields need to "
|
||||
"refresh the access token. You must specify refresh_token, "
|
||||
"token_uri, client_id, and client_secret."
|
||||
)
|
||||
|
||||
(
|
||||
access_token,
|
||||
refresh_token,
|
||||
expiry,
|
||||
grant_response,
|
||||
rapt_token,
|
||||
) = await reauth.refresh_grant(
|
||||
request,
|
||||
self._token_uri,
|
||||
self._refresh_token,
|
||||
self._client_id,
|
||||
self._client_secret,
|
||||
scopes=self._scopes,
|
||||
rapt_token=self._rapt_token,
|
||||
enable_reauth_refresh=self._enable_reauth_refresh,
|
||||
)
|
||||
|
||||
self.token = access_token
|
||||
self.expiry = expiry
|
||||
self._refresh_token = refresh_token
|
||||
self._id_token = grant_response.get("id_token")
|
||||
self._rapt_token = rapt_token
|
||||
|
||||
if self._scopes and "scope" in grant_response:
|
||||
requested_scopes = frozenset(self._scopes)
|
||||
granted_scopes = frozenset(grant_response["scope"].split())
|
||||
scopes_requested_but_not_granted = requested_scopes - granted_scopes
|
||||
if scopes_requested_but_not_granted:
|
||||
raise exceptions.RefreshError(
|
||||
"Not all requested scopes were granted by the "
|
||||
"authorization server, missing scopes {}.".format(
|
||||
", ".join(scopes_requested_but_not_granted)
|
||||
)
|
||||
)
|
||||
|
||||
@_helpers.copy_docstring(credentials.Credentials)
|
||||
async def before_request(self, request, method, url, headers):
|
||||
if not self.valid:
|
||||
await self.refresh(request)
|
||||
self.apply(headers)
|
||||
|
||||
|
||||
class UserAccessTokenCredentials(oauth2_credentials.UserAccessTokenCredentials):
|
||||
"""Access token credentials for user account.
|
||||
|
||||
Obtain the access token for a given user account or the current active
|
||||
user account with the ``gcloud auth print-access-token`` command.
|
||||
|
||||
Args:
|
||||
account (Optional[str]): Account to get the access token for. If not
|
||||
specified, the current active account will be used.
|
||||
quota_project_id (Optional[str]): The project ID used for quota
|
||||
and billing.
|
||||
|
||||
"""
|
||||
@@ -0,0 +1,285 @@
|
||||
# Copyright 2020 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Google ID Token helpers.
|
||||
|
||||
Provides support for verifying `OpenID Connect ID Tokens`_, especially ones
|
||||
generated by Google infrastructure.
|
||||
|
||||
To parse and verify an ID Token issued by Google's OAuth 2.0 authorization
|
||||
server use :func:`verify_oauth2_token`. To verify an ID Token issued by
|
||||
Firebase, use :func:`verify_firebase_token`.
|
||||
|
||||
A general purpose ID Token verifier is available as :func:`verify_token`.
|
||||
|
||||
Example::
|
||||
|
||||
from google.oauth2 import _id_token_async
|
||||
from google.auth.transport import aiohttp_requests
|
||||
|
||||
request = aiohttp_requests.Request()
|
||||
|
||||
id_info = await _id_token_async.verify_oauth2_token(
|
||||
token, request, 'my-client-id.example.com')
|
||||
|
||||
if id_info['iss'] != 'https://accounts.google.com':
|
||||
raise ValueError('Wrong issuer.')
|
||||
|
||||
userid = id_info['sub']
|
||||
|
||||
By default, this will re-fetch certificates for each verification. Because
|
||||
Google's public keys are only changed infrequently (on the order of once per
|
||||
day), you may wish to take advantage of caching to reduce latency and the
|
||||
potential for network errors. This can be accomplished using an external
|
||||
library like `CacheControl`_ to create a cache-aware
|
||||
:class:`google.auth.transport.Request`::
|
||||
|
||||
import cachecontrol
|
||||
import google.auth.transport.requests
|
||||
import requests
|
||||
|
||||
session = requests.session()
|
||||
cached_session = cachecontrol.CacheControl(session)
|
||||
request = google.auth.transport.requests.Request(session=cached_session)
|
||||
|
||||
.. _OpenID Connect ID Token:
|
||||
http://openid.net/specs/openid-connect-core-1_0.html#IDToken
|
||||
.. _CacheControl: https://cachecontrol.readthedocs.io
|
||||
"""
|
||||
|
||||
import http.client as http_client
|
||||
import json
|
||||
import os
|
||||
|
||||
from google.auth import environment_vars
|
||||
from google.auth import exceptions
|
||||
from google.auth import jwt
|
||||
from google.auth.transport import requests
|
||||
from google.oauth2 import id_token as sync_id_token
|
||||
|
||||
|
||||
async def _fetch_certs(request, certs_url):
|
||||
"""Fetches certificates.
|
||||
|
||||
Google-style cerificate endpoints return JSON in the format of
|
||||
``{'key id': 'x509 certificate'}``.
|
||||
|
||||
Args:
|
||||
request (google.auth.transport.Request): The object used to make
|
||||
HTTP requests. This must be an aiohttp request.
|
||||
certs_url (str): The certificate endpoint URL.
|
||||
|
||||
Returns:
|
||||
Mapping[str, str]: A mapping of public key ID to x.509 certificate
|
||||
data.
|
||||
"""
|
||||
response = await request(certs_url, method="GET")
|
||||
|
||||
if response.status != http_client.OK:
|
||||
raise exceptions.TransportError(
|
||||
"Could not fetch certificates at {}".format(certs_url)
|
||||
)
|
||||
|
||||
data = await response.content()
|
||||
|
||||
return json.loads(data)
|
||||
|
||||
|
||||
async def verify_token(
|
||||
id_token,
|
||||
request,
|
||||
audience=None,
|
||||
certs_url=sync_id_token._GOOGLE_OAUTH2_CERTS_URL,
|
||||
clock_skew_in_seconds=0,
|
||||
):
|
||||
"""Verifies an ID token and returns the decoded token.
|
||||
|
||||
Args:
|
||||
id_token (Union[str, bytes]): The encoded token.
|
||||
request (google.auth.transport.Request): The object used to make
|
||||
HTTP requests. This must be an aiohttp request.
|
||||
audience (str): The audience that this token is intended for. If None
|
||||
then the audience is not verified.
|
||||
certs_url (str): The URL that specifies the certificates to use to
|
||||
verify the token. This URL should return JSON in the format of
|
||||
``{'key id': 'x509 certificate'}``.
|
||||
clock_skew_in_seconds (int): The clock skew used for `iat` and `exp`
|
||||
validation.
|
||||
|
||||
Returns:
|
||||
Mapping[str, Any]: The decoded token.
|
||||
"""
|
||||
certs = await _fetch_certs(request, certs_url)
|
||||
|
||||
return jwt.decode(
|
||||
id_token,
|
||||
certs=certs,
|
||||
audience=audience,
|
||||
clock_skew_in_seconds=clock_skew_in_seconds,
|
||||
)
|
||||
|
||||
|
||||
async def verify_oauth2_token(
|
||||
id_token, request, audience=None, clock_skew_in_seconds=0
|
||||
):
|
||||
"""Verifies an ID Token issued by Google's OAuth 2.0 authorization server.
|
||||
|
||||
Args:
|
||||
id_token (Union[str, bytes]): The encoded token.
|
||||
request (google.auth.transport.Request): The object used to make
|
||||
HTTP requests. This must be an aiohttp request.
|
||||
audience (str): The audience that this token is intended for. This is
|
||||
typically your application's OAuth 2.0 client ID. If None then the
|
||||
audience is not verified.
|
||||
clock_skew_in_seconds (int): The clock skew used for `iat` and `exp`
|
||||
validation.
|
||||
|
||||
Returns:
|
||||
Mapping[str, Any]: The decoded token.
|
||||
|
||||
Raises:
|
||||
exceptions.GoogleAuthError: If the issuer is invalid.
|
||||
"""
|
||||
idinfo = await verify_token(
|
||||
id_token,
|
||||
request,
|
||||
audience=audience,
|
||||
certs_url=sync_id_token._GOOGLE_OAUTH2_CERTS_URL,
|
||||
clock_skew_in_seconds=clock_skew_in_seconds,
|
||||
)
|
||||
|
||||
if idinfo["iss"] not in sync_id_token._GOOGLE_ISSUERS:
|
||||
raise exceptions.GoogleAuthError(
|
||||
"Wrong issuer. 'iss' should be one of the following: {}".format(
|
||||
sync_id_token._GOOGLE_ISSUERS
|
||||
)
|
||||
)
|
||||
|
||||
return idinfo
|
||||
|
||||
|
||||
async def verify_firebase_token(
|
||||
id_token, request, audience=None, clock_skew_in_seconds=0
|
||||
):
|
||||
"""Verifies an ID Token issued by Firebase Authentication.
|
||||
|
||||
Args:
|
||||
id_token (Union[str, bytes]): The encoded token.
|
||||
request (google.auth.transport.Request): The object used to make
|
||||
HTTP requests. This must be an aiohttp request.
|
||||
audience (str): The audience that this token is intended for. This is
|
||||
typically your Firebase application ID. If None then the audience
|
||||
is not verified.
|
||||
clock_skew_in_seconds (int): The clock skew used for `iat` and `exp`
|
||||
validation.
|
||||
|
||||
Returns:
|
||||
Mapping[str, Any]: The decoded token.
|
||||
"""
|
||||
return await verify_token(
|
||||
id_token,
|
||||
request,
|
||||
audience=audience,
|
||||
certs_url=sync_id_token._GOOGLE_APIS_CERTS_URL,
|
||||
clock_skew_in_seconds=clock_skew_in_seconds,
|
||||
)
|
||||
|
||||
|
||||
async def fetch_id_token(request, audience):
|
||||
"""Fetch the ID Token from the current environment.
|
||||
|
||||
This function acquires ID token from the environment in the following order.
|
||||
See https://google.aip.dev/auth/4110.
|
||||
|
||||
1. If the environment variable ``GOOGLE_APPLICATION_CREDENTIALS`` is set
|
||||
to the path of a valid service account JSON file, then ID token is
|
||||
acquired using this service account credentials.
|
||||
2. If the application is running in Compute Engine, App Engine or Cloud Run,
|
||||
then the ID token are obtained from the metadata server.
|
||||
3. If metadata server doesn't exist and no valid service account credentials
|
||||
are found, :class:`~google.auth.exceptions.DefaultCredentialsError` will
|
||||
be raised.
|
||||
|
||||
Example::
|
||||
|
||||
import google.oauth2._id_token_async
|
||||
import google.auth.transport.aiohttp_requests
|
||||
|
||||
request = google.auth.transport.aiohttp_requests.Request()
|
||||
target_audience = "https://pubsub.googleapis.com"
|
||||
|
||||
id_token = await google.oauth2._id_token_async.fetch_id_token(request, target_audience)
|
||||
|
||||
Args:
|
||||
request (google.auth.transport.aiohttp_requests.Request): A callable used to make
|
||||
HTTP requests.
|
||||
audience (str): The audience that this ID token is intended for.
|
||||
|
||||
Returns:
|
||||
str: The ID token.
|
||||
|
||||
Raises:
|
||||
~google.auth.exceptions.DefaultCredentialsError:
|
||||
If metadata server doesn't exist and no valid service account
|
||||
credentials are found.
|
||||
"""
|
||||
# 1. Try to get credentials from the GOOGLE_APPLICATION_CREDENTIALS environment
|
||||
# variable.
|
||||
credentials_filename = os.environ.get(environment_vars.CREDENTIALS)
|
||||
if credentials_filename:
|
||||
if not (
|
||||
os.path.exists(credentials_filename)
|
||||
and os.path.isfile(credentials_filename)
|
||||
):
|
||||
raise exceptions.DefaultCredentialsError(
|
||||
"GOOGLE_APPLICATION_CREDENTIALS path is either not found or invalid."
|
||||
)
|
||||
|
||||
try:
|
||||
with open(credentials_filename, "r") as f:
|
||||
from google.oauth2 import _service_account_async as service_account
|
||||
|
||||
info = json.load(f)
|
||||
if info.get("type") == "service_account":
|
||||
credentials = service_account.IDTokenCredentials.from_service_account_info(
|
||||
info, target_audience=audience
|
||||
)
|
||||
await credentials.refresh(request)
|
||||
return credentials.token
|
||||
except ValueError as caught_exc:
|
||||
new_exc = exceptions.DefaultCredentialsError(
|
||||
"GOOGLE_APPLICATION_CREDENTIALS is not valid service account credentials.",
|
||||
caught_exc,
|
||||
)
|
||||
raise new_exc from caught_exc
|
||||
|
||||
# 2. Try to fetch ID token from metada server if it exists. The code works
|
||||
# for GAE and Cloud Run metadata server as well.
|
||||
try:
|
||||
from google.auth import compute_engine
|
||||
from google.auth.compute_engine import _metadata
|
||||
|
||||
request_new = requests.Request()
|
||||
if _metadata.ping(request_new):
|
||||
credentials = compute_engine.IDTokenCredentials(
|
||||
request_new, audience, use_metadata_identity_endpoint=True
|
||||
)
|
||||
credentials.refresh(request_new)
|
||||
return credentials.token
|
||||
except (ImportError, exceptions.TransportError):
|
||||
pass
|
||||
|
||||
raise exceptions.DefaultCredentialsError(
|
||||
"Neither metadata server or valid service account credentials are found."
|
||||
)
|
||||
@@ -0,0 +1,328 @@
|
||||
# Copyright 2021 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""A module that provides functions for handling rapt authentication.
|
||||
|
||||
Reauth is a process of obtaining additional authentication (such as password,
|
||||
security token, etc.) while refreshing OAuth 2.0 credentials for a user.
|
||||
|
||||
Credentials that use the Reauth flow must have the reauth scope,
|
||||
``https://www.googleapis.com/auth/accounts.reauth``.
|
||||
|
||||
This module provides a high-level function for executing the Reauth process,
|
||||
:func:`refresh_grant`, and lower-level helpers for doing the individual
|
||||
steps of the reauth process.
|
||||
|
||||
Those steps are:
|
||||
|
||||
1. Obtaining a list of challenges from the reauth server.
|
||||
2. Running through each challenge and sending the result back to the reauth
|
||||
server.
|
||||
3. Refreshing the access token using the returned rapt token.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
from google.auth import exceptions
|
||||
from google.oauth2 import _client
|
||||
from google.oauth2 import _client_async
|
||||
from google.oauth2 import challenges
|
||||
from google.oauth2 import reauth
|
||||
|
||||
|
||||
async def _get_challenges(
|
||||
request, supported_challenge_types, access_token, requested_scopes=None
|
||||
):
|
||||
"""Does initial request to reauth API to get the challenges.
|
||||
|
||||
Args:
|
||||
request (google.auth.transport.Request): A callable used to make
|
||||
HTTP requests. This must be an aiohttp request.
|
||||
supported_challenge_types (Sequence[str]): list of challenge names
|
||||
supported by the manager.
|
||||
access_token (str): Access token with reauth scopes.
|
||||
requested_scopes (Optional(Sequence[str])): Authorized scopes for the credentials.
|
||||
|
||||
Returns:
|
||||
dict: The response from the reauth API.
|
||||
"""
|
||||
body = {"supportedChallengeTypes": supported_challenge_types}
|
||||
if requested_scopes:
|
||||
body["oauthScopesForDomainPolicyLookup"] = requested_scopes
|
||||
|
||||
return await _client_async._token_endpoint_request(
|
||||
request,
|
||||
reauth._REAUTH_API + ":start",
|
||||
body,
|
||||
access_token=access_token,
|
||||
use_json=True,
|
||||
)
|
||||
|
||||
|
||||
async def _send_challenge_result(
|
||||
request, session_id, challenge_id, client_input, access_token
|
||||
):
|
||||
"""Attempt to refresh access token by sending next challenge result.
|
||||
|
||||
Args:
|
||||
request (google.auth.transport.Request): A callable used to make
|
||||
HTTP requests. This must be an aiohttp request.
|
||||
session_id (str): session id returned by the initial reauth call.
|
||||
challenge_id (str): challenge id returned by the initial reauth call.
|
||||
client_input: dict with a challenge-specific client input. For example:
|
||||
``{'credential': password}`` for password challenge.
|
||||
access_token (str): Access token with reauth scopes.
|
||||
|
||||
Returns:
|
||||
dict: The response from the reauth API.
|
||||
"""
|
||||
body = {
|
||||
"sessionId": session_id,
|
||||
"challengeId": challenge_id,
|
||||
"action": "RESPOND",
|
||||
"proposalResponse": client_input,
|
||||
}
|
||||
|
||||
return await _client_async._token_endpoint_request(
|
||||
request,
|
||||
reauth._REAUTH_API + "/{}:continue".format(session_id),
|
||||
body,
|
||||
access_token=access_token,
|
||||
use_json=True,
|
||||
)
|
||||
|
||||
|
||||
async def _run_next_challenge(msg, request, access_token):
|
||||
"""Get the next challenge from msg and run it.
|
||||
|
||||
Args:
|
||||
msg (dict): Reauth API response body (either from the initial request to
|
||||
https://reauth.googleapis.com/v2/sessions:start or from sending the
|
||||
previous challenge response to
|
||||
https://reauth.googleapis.com/v2/sessions/id:continue)
|
||||
request (google.auth.transport.Request): A callable used to make
|
||||
HTTP requests. This must be an aiohttp request.
|
||||
access_token (str): reauth access token
|
||||
|
||||
Returns:
|
||||
dict: The response from the reauth API.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.ReauthError: if reauth failed.
|
||||
"""
|
||||
for challenge in msg["challenges"]:
|
||||
if challenge["status"] != "READY":
|
||||
# Skip non-activated challenges.
|
||||
continue
|
||||
c = challenges.AVAILABLE_CHALLENGES.get(challenge["challengeType"], None)
|
||||
if not c:
|
||||
raise exceptions.ReauthFailError(
|
||||
"Unsupported challenge type {0}. Supported types: {1}".format(
|
||||
challenge["challengeType"],
|
||||
",".join(list(challenges.AVAILABLE_CHALLENGES.keys())),
|
||||
)
|
||||
)
|
||||
if not c.is_locally_eligible:
|
||||
raise exceptions.ReauthFailError(
|
||||
"Challenge {0} is not locally eligible".format(
|
||||
challenge["challengeType"]
|
||||
)
|
||||
)
|
||||
client_input = c.obtain_challenge_input(challenge)
|
||||
if not client_input:
|
||||
return None
|
||||
return await _send_challenge_result(
|
||||
request,
|
||||
msg["sessionId"],
|
||||
challenge["challengeId"],
|
||||
client_input,
|
||||
access_token,
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
async def _obtain_rapt(request, access_token, requested_scopes):
|
||||
"""Given an http request method and reauth access token, get rapt token.
|
||||
|
||||
Args:
|
||||
request (google.auth.transport.Request): A callable used to make
|
||||
HTTP requests. This must be an aiohttp request.
|
||||
access_token (str): reauth access token
|
||||
requested_scopes (Sequence[str]): scopes required by the client application
|
||||
|
||||
Returns:
|
||||
str: The rapt token.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.ReauthError: if reauth failed
|
||||
"""
|
||||
msg = await _get_challenges(
|
||||
request,
|
||||
list(challenges.AVAILABLE_CHALLENGES.keys()),
|
||||
access_token,
|
||||
requested_scopes,
|
||||
)
|
||||
|
||||
if msg["status"] == reauth._AUTHENTICATED:
|
||||
return msg["encodedProofOfReauthToken"]
|
||||
|
||||
for _ in range(0, reauth.RUN_CHALLENGE_RETRY_LIMIT):
|
||||
if not (
|
||||
msg["status"] == reauth._CHALLENGE_REQUIRED
|
||||
or msg["status"] == reauth._CHALLENGE_PENDING
|
||||
):
|
||||
raise exceptions.ReauthFailError(
|
||||
"Reauthentication challenge failed due to API error: {}".format(
|
||||
msg["status"]
|
||||
)
|
||||
)
|
||||
|
||||
if not reauth.is_interactive():
|
||||
raise exceptions.ReauthFailError(
|
||||
"Reauthentication challenge could not be answered because you are not"
|
||||
" in an interactive session."
|
||||
)
|
||||
|
||||
msg = await _run_next_challenge(msg, request, access_token)
|
||||
|
||||
if msg["status"] == reauth._AUTHENTICATED:
|
||||
return msg["encodedProofOfReauthToken"]
|
||||
|
||||
# If we got here it means we didn't get authenticated.
|
||||
raise exceptions.ReauthFailError("Failed to obtain rapt token.")
|
||||
|
||||
|
||||
async def get_rapt_token(
|
||||
request, client_id, client_secret, refresh_token, token_uri, scopes=None
|
||||
):
|
||||
"""Given an http request method and refresh_token, get rapt token.
|
||||
|
||||
Args:
|
||||
request (google.auth.transport.Request): A callable used to make
|
||||
HTTP requests. This must be an aiohttp request.
|
||||
client_id (str): client id to get access token for reauth scope.
|
||||
client_secret (str): client secret for the client_id
|
||||
refresh_token (str): refresh token to refresh access token
|
||||
token_uri (str): uri to refresh access token
|
||||
scopes (Optional(Sequence[str])): scopes required by the client application
|
||||
|
||||
Returns:
|
||||
str: The rapt token.
|
||||
Raises:
|
||||
google.auth.exceptions.RefreshError: If reauth failed.
|
||||
"""
|
||||
sys.stderr.write("Reauthentication required.\n")
|
||||
|
||||
# Get access token for reauth.
|
||||
access_token, _, _, _ = await _client_async.refresh_grant(
|
||||
request=request,
|
||||
client_id=client_id,
|
||||
client_secret=client_secret,
|
||||
refresh_token=refresh_token,
|
||||
token_uri=token_uri,
|
||||
scopes=[reauth._REAUTH_SCOPE],
|
||||
)
|
||||
|
||||
# Get rapt token from reauth API.
|
||||
rapt_token = await _obtain_rapt(request, access_token, requested_scopes=scopes)
|
||||
|
||||
return rapt_token
|
||||
|
||||
|
||||
async def refresh_grant(
|
||||
request,
|
||||
token_uri,
|
||||
refresh_token,
|
||||
client_id,
|
||||
client_secret,
|
||||
scopes=None,
|
||||
rapt_token=None,
|
||||
enable_reauth_refresh=False,
|
||||
):
|
||||
"""Implements the reauthentication flow.
|
||||
|
||||
Args:
|
||||
request (google.auth.transport.Request): A callable used to make
|
||||
HTTP requests. This must be an aiohttp request.
|
||||
token_uri (str): The OAuth 2.0 authorizations server's token endpoint
|
||||
URI.
|
||||
refresh_token (str): The refresh token to use to get a new access
|
||||
token.
|
||||
client_id (str): The OAuth 2.0 application's client ID.
|
||||
client_secret (str): The Oauth 2.0 appliaction's client secret.
|
||||
scopes (Optional(Sequence[str])): Scopes to request. If present, all
|
||||
scopes must be authorized for the refresh token. Useful if refresh
|
||||
token has a wild card scope (e.g.
|
||||
'https://www.googleapis.com/auth/any-api').
|
||||
rapt_token (Optional(str)): The rapt token for reauth.
|
||||
enable_reauth_refresh (Optional[bool]): Whether reauth refresh flow
|
||||
should be used. The default value is False. This option is for
|
||||
gcloud only, other users should use the default value.
|
||||
|
||||
Returns:
|
||||
Tuple[str, Optional[str], Optional[datetime], Mapping[str, str], str]: The
|
||||
access token, new refresh token, expiration, the additional data
|
||||
returned by the token endpoint, and the rapt token.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.RefreshError: If the token endpoint returned
|
||||
an error.
|
||||
"""
|
||||
body = {
|
||||
"grant_type": _client._REFRESH_GRANT_TYPE,
|
||||
"client_id": client_id,
|
||||
"client_secret": client_secret,
|
||||
"refresh_token": refresh_token,
|
||||
}
|
||||
if scopes:
|
||||
body["scope"] = " ".join(scopes)
|
||||
if rapt_token:
|
||||
body["rapt"] = rapt_token
|
||||
|
||||
response_status_ok, response_data, retryable_error = await _client_async._token_endpoint_request_no_throw(
|
||||
request, token_uri, body
|
||||
)
|
||||
if (
|
||||
not response_status_ok
|
||||
and response_data.get("error") == reauth._REAUTH_NEEDED_ERROR
|
||||
and (
|
||||
response_data.get("error_subtype")
|
||||
== reauth._REAUTH_NEEDED_ERROR_INVALID_RAPT
|
||||
or response_data.get("error_subtype")
|
||||
== reauth._REAUTH_NEEDED_ERROR_RAPT_REQUIRED
|
||||
)
|
||||
):
|
||||
if not enable_reauth_refresh:
|
||||
raise exceptions.RefreshError(
|
||||
"Reauthentication is needed. Please run `gcloud auth application-default login` to reauthenticate."
|
||||
)
|
||||
|
||||
rapt_token = await get_rapt_token(
|
||||
request, client_id, client_secret, refresh_token, token_uri, scopes=scopes
|
||||
)
|
||||
body["rapt"] = rapt_token
|
||||
(
|
||||
response_status_ok,
|
||||
response_data,
|
||||
retryable_error,
|
||||
) = await _client_async._token_endpoint_request_no_throw(
|
||||
request, token_uri, body
|
||||
)
|
||||
|
||||
if not response_status_ok:
|
||||
_client._handle_error_response(response_data, retryable_error)
|
||||
refresh_response = _client._handle_refresh_grant_response(
|
||||
response_data, refresh_token
|
||||
)
|
||||
return refresh_response + (rapt_token,)
|
||||
@@ -0,0 +1,132 @@
|
||||
# Copyright 2020 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Service Accounts: JSON Web Token (JWT) Profile for OAuth 2.0
|
||||
|
||||
NOTE: This file adds asynchronous refresh methods to both credentials
|
||||
classes, and therefore async/await syntax is required when calling this
|
||||
method when using service account credentials with asynchronous functionality.
|
||||
Otherwise, all other methods are inherited from the regular service account
|
||||
credentials file google.oauth2.service_account
|
||||
|
||||
"""
|
||||
|
||||
from google.auth import _credentials_async as credentials_async
|
||||
from google.auth import _helpers
|
||||
from google.oauth2 import _client_async
|
||||
from google.oauth2 import service_account
|
||||
|
||||
|
||||
class Credentials(
|
||||
service_account.Credentials, credentials_async.Scoped, credentials_async.Credentials
|
||||
):
|
||||
"""Service account credentials
|
||||
|
||||
Usually, you'll create these credentials with one of the helper
|
||||
constructors. To create credentials using a Google service account
|
||||
private key JSON file::
|
||||
|
||||
credentials = _service_account_async.Credentials.from_service_account_file(
|
||||
'service-account.json')
|
||||
|
||||
Or if you already have the service account file loaded::
|
||||
|
||||
service_account_info = json.load(open('service_account.json'))
|
||||
credentials = _service_account_async.Credentials.from_service_account_info(
|
||||
service_account_info)
|
||||
|
||||
Both helper methods pass on arguments to the constructor, so you can
|
||||
specify additional scopes and a subject if necessary::
|
||||
|
||||
credentials = _service_account_async.Credentials.from_service_account_file(
|
||||
'service-account.json',
|
||||
scopes=['email'],
|
||||
subject='user@example.com')
|
||||
|
||||
The credentials are considered immutable. If you want to modify the scopes
|
||||
or the subject used for delegation, use :meth:`with_scopes` or
|
||||
:meth:`with_subject`::
|
||||
|
||||
scoped_credentials = credentials.with_scopes(['email'])
|
||||
delegated_credentials = credentials.with_subject(subject)
|
||||
|
||||
To add a quota project, use :meth:`with_quota_project`::
|
||||
|
||||
credentials = credentials.with_quota_project('myproject-123')
|
||||
"""
|
||||
|
||||
@_helpers.copy_docstring(credentials_async.Credentials)
|
||||
async def refresh(self, request):
|
||||
assertion = self._make_authorization_grant_assertion()
|
||||
access_token, expiry, _ = await _client_async.jwt_grant(
|
||||
request, self._token_uri, assertion
|
||||
)
|
||||
self.token = access_token
|
||||
self.expiry = expiry
|
||||
|
||||
|
||||
class IDTokenCredentials(
|
||||
service_account.IDTokenCredentials,
|
||||
credentials_async.Signing,
|
||||
credentials_async.Credentials,
|
||||
):
|
||||
"""Open ID Connect ID Token-based service account credentials.
|
||||
|
||||
These credentials are largely similar to :class:`.Credentials`, but instead
|
||||
of using an OAuth 2.0 Access Token as the bearer token, they use an Open
|
||||
ID Connect ID Token as the bearer token. These credentials are useful when
|
||||
communicating to services that require ID Tokens and can not accept access
|
||||
tokens.
|
||||
|
||||
Usually, you'll create these credentials with one of the helper
|
||||
constructors. To create credentials using a Google service account
|
||||
private key JSON file::
|
||||
|
||||
credentials = (
|
||||
_service_account_async.IDTokenCredentials.from_service_account_file(
|
||||
'service-account.json'))
|
||||
|
||||
Or if you already have the service account file loaded::
|
||||
|
||||
service_account_info = json.load(open('service_account.json'))
|
||||
credentials = (
|
||||
_service_account_async.IDTokenCredentials.from_service_account_info(
|
||||
service_account_info))
|
||||
|
||||
Both helper methods pass on arguments to the constructor, so you can
|
||||
specify additional scopes and a subject if necessary::
|
||||
|
||||
credentials = (
|
||||
_service_account_async.IDTokenCredentials.from_service_account_file(
|
||||
'service-account.json',
|
||||
scopes=['email'],
|
||||
subject='user@example.com'))
|
||||
|
||||
The credentials are considered immutable. If you want to modify the scopes
|
||||
or the subject used for delegation, use :meth:`with_scopes` or
|
||||
:meth:`with_subject`::
|
||||
|
||||
scoped_credentials = credentials.with_scopes(['email'])
|
||||
delegated_credentials = credentials.with_subject(subject)
|
||||
|
||||
"""
|
||||
|
||||
@_helpers.copy_docstring(credentials_async.Credentials)
|
||||
async def refresh(self, request):
|
||||
assertion = self._make_authorization_grant_assertion()
|
||||
access_token, expiry, _ = await _client_async.id_token_jwt_grant(
|
||||
request, self._token_uri, assertion
|
||||
)
|
||||
self.token = access_token
|
||||
self.expiry = expiry
|
||||
281
.venv/lib/python3.10/site-packages/google/oauth2/challenges.py
Normal file
281
.venv/lib/python3.10/site-packages/google/oauth2/challenges.py
Normal file
@@ -0,0 +1,281 @@
|
||||
# Copyright 2021 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
""" Challenges for reauthentication.
|
||||
"""
|
||||
|
||||
import abc
|
||||
import base64
|
||||
import getpass
|
||||
import sys
|
||||
|
||||
from google.auth import _helpers
|
||||
from google.auth import exceptions
|
||||
from google.oauth2 import webauthn_handler_factory
|
||||
from google.oauth2.webauthn_types import (
|
||||
AuthenticationExtensionsClientInputs,
|
||||
GetRequest,
|
||||
PublicKeyCredentialDescriptor,
|
||||
)
|
||||
|
||||
|
||||
REAUTH_ORIGIN = "https://accounts.google.com"
|
||||
SAML_CHALLENGE_MESSAGE = (
|
||||
"Please run `gcloud auth login` to complete reauthentication with SAML."
|
||||
)
|
||||
WEBAUTHN_TIMEOUT_MS = 120000 # Two minute timeout
|
||||
|
||||
|
||||
def get_user_password(text):
|
||||
"""Get password from user.
|
||||
|
||||
Override this function with a different logic if you are using this library
|
||||
outside a CLI.
|
||||
|
||||
Args:
|
||||
text (str): message for the password prompt.
|
||||
|
||||
Returns:
|
||||
str: password string.
|
||||
"""
|
||||
return getpass.getpass(text)
|
||||
|
||||
|
||||
class ReauthChallenge(metaclass=abc.ABCMeta):
|
||||
"""Base class for reauth challenges."""
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def name(self): # pragma: NO COVER
|
||||
"""Returns the name of the challenge."""
|
||||
raise NotImplementedError("name property must be implemented")
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def is_locally_eligible(self): # pragma: NO COVER
|
||||
"""Returns true if a challenge is supported locally on this machine."""
|
||||
raise NotImplementedError("is_locally_eligible property must be implemented")
|
||||
|
||||
@abc.abstractmethod
|
||||
def obtain_challenge_input(self, metadata): # pragma: NO COVER
|
||||
"""Performs logic required to obtain credentials and returns it.
|
||||
|
||||
Args:
|
||||
metadata (Mapping): challenge metadata returned in the 'challenges' field in
|
||||
the initial reauth request. Includes the 'challengeType' field
|
||||
and other challenge-specific fields.
|
||||
|
||||
Returns:
|
||||
response that will be send to the reauth service as the content of
|
||||
the 'proposalResponse' field in the request body. Usually a dict
|
||||
with the keys specific to the challenge. For example,
|
||||
``{'credential': password}`` for password challenge.
|
||||
"""
|
||||
raise NotImplementedError("obtain_challenge_input method must be implemented")
|
||||
|
||||
|
||||
class PasswordChallenge(ReauthChallenge):
|
||||
"""Challenge that asks for user's password."""
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return "PASSWORD"
|
||||
|
||||
@property
|
||||
def is_locally_eligible(self):
|
||||
return True
|
||||
|
||||
@_helpers.copy_docstring(ReauthChallenge)
|
||||
def obtain_challenge_input(self, unused_metadata):
|
||||
passwd = get_user_password("Please enter your password:")
|
||||
if not passwd:
|
||||
passwd = " " # avoid the server crashing in case of no password :D
|
||||
return {"credential": passwd}
|
||||
|
||||
|
||||
class SecurityKeyChallenge(ReauthChallenge):
|
||||
"""Challenge that asks for user's security key touch."""
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return "SECURITY_KEY"
|
||||
|
||||
@property
|
||||
def is_locally_eligible(self):
|
||||
return True
|
||||
|
||||
@_helpers.copy_docstring(ReauthChallenge)
|
||||
def obtain_challenge_input(self, metadata):
|
||||
# Check if there is an available Webauthn Handler, if not use pyu2f
|
||||
try:
|
||||
factory = webauthn_handler_factory.WebauthnHandlerFactory()
|
||||
webauthn_handler = factory.get_handler()
|
||||
if webauthn_handler is not None:
|
||||
sys.stderr.write("Please insert and touch your security key\n")
|
||||
return self._obtain_challenge_input_webauthn(metadata, webauthn_handler)
|
||||
except Exception:
|
||||
# Attempt pyu2f if exception in webauthn flow
|
||||
pass
|
||||
|
||||
try:
|
||||
import pyu2f.convenience.authenticator # type: ignore
|
||||
import pyu2f.errors # type: ignore
|
||||
import pyu2f.model # type: ignore
|
||||
except ImportError:
|
||||
raise exceptions.ReauthFailError(
|
||||
"pyu2f dependency is required to use Security key reauth feature. "
|
||||
"It can be installed via `pip install pyu2f` or `pip install google-auth[reauth]`."
|
||||
)
|
||||
sk = metadata["securityKey"]
|
||||
challenges = sk["challenges"]
|
||||
# Read both 'applicationId' and 'relyingPartyId', if they are the same, use
|
||||
# applicationId, if they are different, use relyingPartyId first and retry
|
||||
# with applicationId
|
||||
application_id = sk["applicationId"]
|
||||
relying_party_id = sk["relyingPartyId"]
|
||||
|
||||
if application_id != relying_party_id:
|
||||
application_parameters = [relying_party_id, application_id]
|
||||
else:
|
||||
application_parameters = [application_id]
|
||||
|
||||
challenge_data = []
|
||||
for c in challenges:
|
||||
kh = c["keyHandle"].encode("ascii")
|
||||
key = pyu2f.model.RegisteredKey(bytearray(base64.urlsafe_b64decode(kh)))
|
||||
challenge = c["challenge"].encode("ascii")
|
||||
challenge = base64.urlsafe_b64decode(challenge)
|
||||
challenge_data.append({"key": key, "challenge": challenge})
|
||||
|
||||
# Track number of tries to suppress error message until all application_parameters
|
||||
# are tried.
|
||||
tries = 0
|
||||
for app_id in application_parameters:
|
||||
try:
|
||||
tries += 1
|
||||
api = pyu2f.convenience.authenticator.CreateCompositeAuthenticator(
|
||||
REAUTH_ORIGIN
|
||||
)
|
||||
response = api.Authenticate(
|
||||
app_id, challenge_data, print_callback=sys.stderr.write
|
||||
)
|
||||
return {"securityKey": response}
|
||||
except pyu2f.errors.U2FError as e:
|
||||
if e.code == pyu2f.errors.U2FError.DEVICE_INELIGIBLE:
|
||||
# Only show error if all app_ids have been tried
|
||||
if tries == len(application_parameters):
|
||||
sys.stderr.write("Ineligible security key.\n")
|
||||
return None
|
||||
continue
|
||||
if e.code == pyu2f.errors.U2FError.TIMEOUT:
|
||||
sys.stderr.write(
|
||||
"Timed out while waiting for security key touch.\n"
|
||||
)
|
||||
else:
|
||||
raise e
|
||||
except pyu2f.errors.PluginError as e:
|
||||
sys.stderr.write("Plugin error: {}.\n".format(e))
|
||||
continue
|
||||
except pyu2f.errors.NoDeviceFoundError:
|
||||
sys.stderr.write("No security key found.\n")
|
||||
return None
|
||||
|
||||
def _obtain_challenge_input_webauthn(self, metadata, webauthn_handler):
|
||||
sk = metadata.get("securityKey")
|
||||
if sk is None:
|
||||
raise exceptions.InvalidValue("securityKey is None")
|
||||
challenges = sk.get("challenges")
|
||||
application_id = sk.get("applicationId")
|
||||
relying_party_id = sk.get("relyingPartyId")
|
||||
if challenges is None or len(challenges) < 1:
|
||||
raise exceptions.InvalidValue("challenges is None or empty")
|
||||
if application_id is None:
|
||||
raise exceptions.InvalidValue("application_id is None")
|
||||
if relying_party_id is None:
|
||||
raise exceptions.InvalidValue("relying_party_id is None")
|
||||
|
||||
allow_credentials = []
|
||||
for challenge in challenges:
|
||||
kh = challenge.get("keyHandle")
|
||||
if kh is None:
|
||||
raise exceptions.InvalidValue("keyHandle is None")
|
||||
key_handle = self._unpadded_urlsafe_b64recode(kh)
|
||||
allow_credentials.append(PublicKeyCredentialDescriptor(id=key_handle))
|
||||
|
||||
extension = AuthenticationExtensionsClientInputs(appid=application_id)
|
||||
|
||||
challenge = challenges[0].get("challenge")
|
||||
if challenge is None:
|
||||
raise exceptions.InvalidValue("challenge is None")
|
||||
|
||||
get_request = GetRequest(
|
||||
origin=REAUTH_ORIGIN,
|
||||
rpid=relying_party_id,
|
||||
challenge=self._unpadded_urlsafe_b64recode(challenge),
|
||||
timeout_ms=WEBAUTHN_TIMEOUT_MS,
|
||||
allow_credentials=allow_credentials,
|
||||
user_verification="required",
|
||||
extensions=extension,
|
||||
)
|
||||
|
||||
try:
|
||||
get_response = webauthn_handler.get(get_request)
|
||||
except Exception as e:
|
||||
sys.stderr.write("Webauthn Error: {}.\n".format(e))
|
||||
raise e
|
||||
|
||||
response = {
|
||||
"clientData": get_response.response.client_data_json,
|
||||
"authenticatorData": get_response.response.authenticator_data,
|
||||
"signatureData": get_response.response.signature,
|
||||
"applicationId": application_id,
|
||||
"keyHandle": get_response.id,
|
||||
"securityKeyReplyType": 2,
|
||||
}
|
||||
return {"securityKey": response}
|
||||
|
||||
def _unpadded_urlsafe_b64recode(self, s):
|
||||
"""Converts standard b64 encoded string to url safe b64 encoded string
|
||||
with no padding."""
|
||||
b = base64.urlsafe_b64decode(s)
|
||||
return base64.urlsafe_b64encode(b).decode().rstrip("=")
|
||||
|
||||
|
||||
class SamlChallenge(ReauthChallenge):
|
||||
"""Challenge that asks the users to browse to their ID Providers.
|
||||
|
||||
Currently SAML challenge is not supported. When obtaining the challenge
|
||||
input, exception will be raised to instruct the users to run
|
||||
`gcloud auth login` for reauthentication.
|
||||
"""
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return "SAML"
|
||||
|
||||
@property
|
||||
def is_locally_eligible(self):
|
||||
return True
|
||||
|
||||
def obtain_challenge_input(self, metadata):
|
||||
# Magic Arch has not fully supported returning a proper dedirect URL
|
||||
# for programmatic SAML users today. So we error our here and request
|
||||
# users to use gcloud to complete a login.
|
||||
raise exceptions.ReauthSamlChallengeFailError(SAML_CHALLENGE_MESSAGE)
|
||||
|
||||
|
||||
AVAILABLE_CHALLENGES = {
|
||||
challenge.name: challenge
|
||||
for challenge in [SecurityKeyChallenge(), PasswordChallenge(), SamlChallenge()]
|
||||
}
|
||||
614
.venv/lib/python3.10/site-packages/google/oauth2/credentials.py
Normal file
614
.venv/lib/python3.10/site-packages/google/oauth2/credentials.py
Normal file
@@ -0,0 +1,614 @@
|
||||
# Copyright 2016 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""OAuth 2.0 Credentials.
|
||||
|
||||
This module provides credentials based on OAuth 2.0 access and refresh tokens.
|
||||
These credentials usually access resources on behalf of a user (resource
|
||||
owner).
|
||||
|
||||
Specifically, this is intended to use access tokens acquired using the
|
||||
`Authorization Code grant`_ and can refresh those tokens using a
|
||||
optional `refresh token`_.
|
||||
|
||||
Obtaining the initial access and refresh token is outside of the scope of this
|
||||
module. Consult `rfc6749 section 4.1`_ for complete details on the
|
||||
Authorization Code grant flow.
|
||||
|
||||
.. _Authorization Code grant: https://tools.ietf.org/html/rfc6749#section-1.3.1
|
||||
.. _refresh token: https://tools.ietf.org/html/rfc6749#section-6
|
||||
.. _rfc6749 section 4.1: https://tools.ietf.org/html/rfc6749#section-4.1
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
import warnings
|
||||
|
||||
from google.auth import _cloud_sdk
|
||||
from google.auth import _helpers
|
||||
from google.auth import credentials
|
||||
from google.auth import exceptions
|
||||
from google.auth import metrics
|
||||
from google.oauth2 import reauth
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# The Google OAuth 2.0 token endpoint. Used for authorized user credentials.
|
||||
_GOOGLE_OAUTH2_TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token"
|
||||
|
||||
# The Google OAuth 2.0 token info endpoint. Used for getting token info JSON from access tokens.
|
||||
_GOOGLE_OAUTH2_TOKEN_INFO_ENDPOINT = "https://oauth2.googleapis.com/tokeninfo"
|
||||
|
||||
|
||||
class Credentials(credentials.ReadOnlyScoped, credentials.CredentialsWithQuotaProject):
|
||||
"""Credentials using OAuth 2.0 access and refresh tokens.
|
||||
|
||||
The credentials are considered immutable except the tokens and the token
|
||||
expiry, which are updated after refresh. If you want to modify the quota
|
||||
project, use :meth:`with_quota_project` or ::
|
||||
|
||||
credentials = credentials.with_quota_project('myproject-123')
|
||||
|
||||
Reauth is disabled by default. To enable reauth, set the
|
||||
`enable_reauth_refresh` parameter to True in the constructor. Note that
|
||||
reauth feature is intended for gcloud to use only.
|
||||
If reauth is enabled, `pyu2f` dependency has to be installed in order to use security
|
||||
key reauth feature. Dependency can be installed via `pip install pyu2f` or `pip install
|
||||
google-auth[reauth]`.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
token,
|
||||
refresh_token=None,
|
||||
id_token=None,
|
||||
token_uri=None,
|
||||
client_id=None,
|
||||
client_secret=None,
|
||||
scopes=None,
|
||||
default_scopes=None,
|
||||
quota_project_id=None,
|
||||
expiry=None,
|
||||
rapt_token=None,
|
||||
refresh_handler=None,
|
||||
enable_reauth_refresh=False,
|
||||
granted_scopes=None,
|
||||
trust_boundary=None,
|
||||
universe_domain=credentials.DEFAULT_UNIVERSE_DOMAIN,
|
||||
account=None,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
token (Optional(str)): The OAuth 2.0 access token. Can be None
|
||||
if refresh information is provided.
|
||||
refresh_token (str): The OAuth 2.0 refresh token. If specified,
|
||||
credentials can be refreshed.
|
||||
id_token (str): The Open ID Connect ID Token.
|
||||
token_uri (str): The OAuth 2.0 authorization server's token
|
||||
endpoint URI. Must be specified for refresh, can be left as
|
||||
None if the token can not be refreshed.
|
||||
client_id (str): The OAuth 2.0 client ID. Must be specified for
|
||||
refresh, can be left as None if the token can not be refreshed.
|
||||
client_secret(str): The OAuth 2.0 client secret. Must be specified
|
||||
for refresh, can be left as None if the token can not be
|
||||
refreshed.
|
||||
scopes (Sequence[str]): The scopes used to obtain authorization.
|
||||
This parameter is used by :meth:`has_scopes`. OAuth 2.0
|
||||
credentials can not request additional scopes after
|
||||
authorization. The scopes must be derivable from the refresh
|
||||
token if refresh information is provided (e.g. The refresh
|
||||
token scopes are a superset of this or contain a wild card
|
||||
scope like 'https://www.googleapis.com/auth/any-api').
|
||||
default_scopes (Sequence[str]): Default scopes passed by a
|
||||
Google client library. Use 'scopes' for user-defined scopes.
|
||||
quota_project_id (Optional[str]): The project ID used for quota and billing.
|
||||
This project may be different from the project used to
|
||||
create the credentials.
|
||||
rapt_token (Optional[str]): The reauth Proof Token.
|
||||
refresh_handler (Optional[Callable[[google.auth.transport.Request, Sequence[str]], [str, datetime]]]):
|
||||
A callable which takes in the HTTP request callable and the list of
|
||||
OAuth scopes and when called returns an access token string for the
|
||||
requested scopes and its expiry datetime. This is useful when no
|
||||
refresh tokens are provided and tokens are obtained by calling
|
||||
some external process on demand. It is particularly useful for
|
||||
retrieving downscoped tokens from a token broker.
|
||||
enable_reauth_refresh (Optional[bool]): Whether reauth refresh flow
|
||||
should be used. This flag is for gcloud to use only.
|
||||
granted_scopes (Optional[Sequence[str]]): The scopes that were consented/granted by the user.
|
||||
This could be different from the requested scopes and it could be empty if granted
|
||||
and requested scopes were same.
|
||||
trust_boundary (str): String representation of trust boundary meta.
|
||||
universe_domain (Optional[str]): The universe domain. The default
|
||||
universe domain is googleapis.com.
|
||||
account (Optional[str]): The account associated with the credential.
|
||||
"""
|
||||
super(Credentials, self).__init__()
|
||||
self.token = token
|
||||
self.expiry = expiry
|
||||
self._refresh_token = refresh_token
|
||||
self._id_token = id_token
|
||||
self._scopes = scopes
|
||||
self._default_scopes = default_scopes
|
||||
self._granted_scopes = granted_scopes
|
||||
self._token_uri = token_uri
|
||||
self._client_id = client_id
|
||||
self._client_secret = client_secret
|
||||
self._quota_project_id = quota_project_id
|
||||
self._rapt_token = rapt_token
|
||||
self.refresh_handler = refresh_handler
|
||||
self._enable_reauth_refresh = enable_reauth_refresh
|
||||
self._trust_boundary = trust_boundary
|
||||
self._universe_domain = universe_domain or credentials.DEFAULT_UNIVERSE_DOMAIN
|
||||
self._account = account or ""
|
||||
self._cred_file_path = None
|
||||
|
||||
def __getstate__(self):
|
||||
"""A __getstate__ method must exist for the __setstate__ to be called
|
||||
This is identical to the default implementation.
|
||||
See https://docs.python.org/3.7/library/pickle.html#object.__setstate__
|
||||
"""
|
||||
state_dict = self.__dict__.copy()
|
||||
# Remove _refresh_handler function as there are limitations pickling and
|
||||
# unpickling certain callables (lambda, functools.partial instances)
|
||||
# because they need to be importable.
|
||||
# Instead, the refresh_handler setter should be used to repopulate this.
|
||||
if "_refresh_handler" in state_dict:
|
||||
del state_dict["_refresh_handler"]
|
||||
|
||||
if "_refresh_worker" in state_dict:
|
||||
del state_dict["_refresh_worker"]
|
||||
return state_dict
|
||||
|
||||
def __setstate__(self, d):
|
||||
"""Credentials pickled with older versions of the class do not have
|
||||
all the attributes."""
|
||||
self.token = d.get("token")
|
||||
self.expiry = d.get("expiry")
|
||||
self._refresh_token = d.get("_refresh_token")
|
||||
self._id_token = d.get("_id_token")
|
||||
self._scopes = d.get("_scopes")
|
||||
self._default_scopes = d.get("_default_scopes")
|
||||
self._granted_scopes = d.get("_granted_scopes")
|
||||
self._token_uri = d.get("_token_uri")
|
||||
self._client_id = d.get("_client_id")
|
||||
self._client_secret = d.get("_client_secret")
|
||||
self._quota_project_id = d.get("_quota_project_id")
|
||||
self._rapt_token = d.get("_rapt_token")
|
||||
self._enable_reauth_refresh = d.get("_enable_reauth_refresh")
|
||||
self._trust_boundary = d.get("_trust_boundary")
|
||||
self._universe_domain = (
|
||||
d.get("_universe_domain") or credentials.DEFAULT_UNIVERSE_DOMAIN
|
||||
)
|
||||
self._cred_file_path = d.get("_cred_file_path")
|
||||
# The refresh_handler setter should be used to repopulate this.
|
||||
self._refresh_handler = None
|
||||
self._refresh_worker = None
|
||||
self._use_non_blocking_refresh = d.get("_use_non_blocking_refresh", False)
|
||||
self._account = d.get("_account", "")
|
||||
|
||||
@property
|
||||
def refresh_token(self):
|
||||
"""Optional[str]: The OAuth 2.0 refresh token."""
|
||||
return self._refresh_token
|
||||
|
||||
@property
|
||||
def scopes(self):
|
||||
"""Optional[str]: The OAuth 2.0 permission scopes."""
|
||||
return self._scopes
|
||||
|
||||
@property
|
||||
def granted_scopes(self):
|
||||
"""Optional[Sequence[str]]: The OAuth 2.0 permission scopes that were granted by the user."""
|
||||
return self._granted_scopes
|
||||
|
||||
@property
|
||||
def token_uri(self):
|
||||
"""Optional[str]: The OAuth 2.0 authorization server's token endpoint
|
||||
URI."""
|
||||
return self._token_uri
|
||||
|
||||
@property
|
||||
def id_token(self):
|
||||
"""Optional[str]: The Open ID Connect ID Token.
|
||||
|
||||
Depending on the authorization server and the scopes requested, this
|
||||
may be populated when credentials are obtained and updated when
|
||||
:meth:`refresh` is called. This token is a JWT. It can be verified
|
||||
and decoded using :func:`google.oauth2.id_token.verify_oauth2_token`.
|
||||
"""
|
||||
return self._id_token
|
||||
|
||||
@property
|
||||
def client_id(self):
|
||||
"""Optional[str]: The OAuth 2.0 client ID."""
|
||||
return self._client_id
|
||||
|
||||
@property
|
||||
def client_secret(self):
|
||||
"""Optional[str]: The OAuth 2.0 client secret."""
|
||||
return self._client_secret
|
||||
|
||||
@property
|
||||
def requires_scopes(self):
|
||||
"""False: OAuth 2.0 credentials have their scopes set when
|
||||
the initial token is requested and can not be changed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def rapt_token(self):
|
||||
"""Optional[str]: The reauth Proof Token."""
|
||||
return self._rapt_token
|
||||
|
||||
@property
|
||||
def refresh_handler(self):
|
||||
"""Returns the refresh handler if available.
|
||||
|
||||
Returns:
|
||||
Optional[Callable[[google.auth.transport.Request, Sequence[str]], [str, datetime]]]:
|
||||
The current refresh handler.
|
||||
"""
|
||||
return self._refresh_handler
|
||||
|
||||
@refresh_handler.setter
|
||||
def refresh_handler(self, value):
|
||||
"""Updates the current refresh handler.
|
||||
|
||||
Args:
|
||||
value (Optional[Callable[[google.auth.transport.Request, Sequence[str]], [str, datetime]]]):
|
||||
The updated value of the refresh handler.
|
||||
|
||||
Raises:
|
||||
TypeError: If the value is not a callable or None.
|
||||
"""
|
||||
if not callable(value) and value is not None:
|
||||
raise TypeError("The provided refresh_handler is not a callable or None.")
|
||||
self._refresh_handler = value
|
||||
|
||||
@property
|
||||
def account(self):
|
||||
"""str: The user account associated with the credential. If the account is unknown an empty string is returned."""
|
||||
return self._account
|
||||
|
||||
def _make_copy(self):
|
||||
cred = self.__class__(
|
||||
self.token,
|
||||
refresh_token=self.refresh_token,
|
||||
id_token=self.id_token,
|
||||
token_uri=self.token_uri,
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
scopes=self.scopes,
|
||||
default_scopes=self.default_scopes,
|
||||
granted_scopes=self.granted_scopes,
|
||||
quota_project_id=self.quota_project_id,
|
||||
rapt_token=self.rapt_token,
|
||||
enable_reauth_refresh=self._enable_reauth_refresh,
|
||||
trust_boundary=self._trust_boundary,
|
||||
universe_domain=self._universe_domain,
|
||||
account=self._account,
|
||||
)
|
||||
cred._cred_file_path = self._cred_file_path
|
||||
return cred
|
||||
|
||||
@_helpers.copy_docstring(credentials.Credentials)
|
||||
def get_cred_info(self):
|
||||
if self._cred_file_path:
|
||||
cred_info = {
|
||||
"credential_source": self._cred_file_path,
|
||||
"credential_type": "user credentials",
|
||||
}
|
||||
if self.account:
|
||||
cred_info["principal"] = self.account
|
||||
return cred_info
|
||||
return None
|
||||
|
||||
@_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
|
||||
def with_quota_project(self, quota_project_id):
|
||||
cred = self._make_copy()
|
||||
cred._quota_project_id = quota_project_id
|
||||
return cred
|
||||
|
||||
@_helpers.copy_docstring(credentials.CredentialsWithTokenUri)
|
||||
def with_token_uri(self, token_uri):
|
||||
cred = self._make_copy()
|
||||
cred._token_uri = token_uri
|
||||
return cred
|
||||
|
||||
def with_account(self, account):
|
||||
"""Returns a copy of these credentials with a modified account.
|
||||
|
||||
Args:
|
||||
account (str): The account to set
|
||||
|
||||
Returns:
|
||||
google.oauth2.credentials.Credentials: A new credentials instance.
|
||||
"""
|
||||
cred = self._make_copy()
|
||||
cred._account = account
|
||||
return cred
|
||||
|
||||
@_helpers.copy_docstring(credentials.CredentialsWithUniverseDomain)
|
||||
def with_universe_domain(self, universe_domain):
|
||||
cred = self._make_copy()
|
||||
cred._universe_domain = universe_domain
|
||||
return cred
|
||||
|
||||
def _metric_header_for_usage(self):
|
||||
return metrics.CRED_TYPE_USER
|
||||
|
||||
@_helpers.copy_docstring(credentials.Credentials)
|
||||
def refresh(self, request):
|
||||
if self._universe_domain != credentials.DEFAULT_UNIVERSE_DOMAIN:
|
||||
raise exceptions.RefreshError(
|
||||
"User credential refresh is only supported in the default "
|
||||
"googleapis.com universe domain, but the current universe "
|
||||
"domain is {}. If you created the credential with an access "
|
||||
"token, it's likely that the provided token is expired now, "
|
||||
"please update your code with a valid token.".format(
|
||||
self._universe_domain
|
||||
)
|
||||
)
|
||||
|
||||
scopes = self._scopes if self._scopes is not None else self._default_scopes
|
||||
# Use refresh handler if available and no refresh token is
|
||||
# available. This is useful in general when tokens are obtained by calling
|
||||
# some external process on demand. It is particularly useful for retrieving
|
||||
# downscoped tokens from a token broker.
|
||||
if self._refresh_token is None and self.refresh_handler:
|
||||
token, expiry = self.refresh_handler(request, scopes=scopes)
|
||||
# Validate returned data.
|
||||
if not isinstance(token, str):
|
||||
raise exceptions.RefreshError(
|
||||
"The refresh_handler returned token is not a string."
|
||||
)
|
||||
if not isinstance(expiry, datetime):
|
||||
raise exceptions.RefreshError(
|
||||
"The refresh_handler returned expiry is not a datetime object."
|
||||
)
|
||||
if _helpers.utcnow() >= expiry - _helpers.REFRESH_THRESHOLD:
|
||||
raise exceptions.RefreshError(
|
||||
"The credentials returned by the refresh_handler are "
|
||||
"already expired."
|
||||
)
|
||||
self.token = token
|
||||
self.expiry = expiry
|
||||
return
|
||||
|
||||
if (
|
||||
self._refresh_token is None
|
||||
or self._token_uri is None
|
||||
or self._client_id is None
|
||||
or self._client_secret is None
|
||||
):
|
||||
raise exceptions.RefreshError(
|
||||
"The credentials do not contain the necessary fields need to "
|
||||
"refresh the access token. You must specify refresh_token, "
|
||||
"token_uri, client_id, and client_secret."
|
||||
)
|
||||
|
||||
(
|
||||
access_token,
|
||||
refresh_token,
|
||||
expiry,
|
||||
grant_response,
|
||||
rapt_token,
|
||||
) = reauth.refresh_grant(
|
||||
request,
|
||||
self._token_uri,
|
||||
self._refresh_token,
|
||||
self._client_id,
|
||||
self._client_secret,
|
||||
scopes=scopes,
|
||||
rapt_token=self._rapt_token,
|
||||
enable_reauth_refresh=self._enable_reauth_refresh,
|
||||
)
|
||||
|
||||
self.token = access_token
|
||||
self.expiry = expiry
|
||||
self._refresh_token = refresh_token
|
||||
self._id_token = grant_response.get("id_token")
|
||||
self._rapt_token = rapt_token
|
||||
|
||||
if scopes and "scope" in grant_response:
|
||||
requested_scopes = frozenset(scopes)
|
||||
self._granted_scopes = grant_response["scope"].split()
|
||||
granted_scopes = frozenset(self._granted_scopes)
|
||||
scopes_requested_but_not_granted = requested_scopes - granted_scopes
|
||||
if scopes_requested_but_not_granted:
|
||||
# User might be presented with unbundled scopes at the time of
|
||||
# consent. So it is a valid scenario to not have all the requested
|
||||
# scopes as part of granted scopes but log a warning in case the
|
||||
# developer wants to debug the scenario.
|
||||
_LOGGER.warning(
|
||||
"Not all requested scopes were granted by the "
|
||||
"authorization server, missing scopes {}.".format(
|
||||
", ".join(scopes_requested_but_not_granted)
|
||||
)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_authorized_user_info(cls, info, scopes=None):
|
||||
"""Creates a Credentials instance from parsed authorized user info.
|
||||
|
||||
Args:
|
||||
info (Mapping[str, str]): The authorized user info in Google
|
||||
format.
|
||||
scopes (Sequence[str]): Optional list of scopes to include in the
|
||||
credentials.
|
||||
|
||||
Returns:
|
||||
google.oauth2.credentials.Credentials: The constructed
|
||||
credentials.
|
||||
|
||||
Raises:
|
||||
ValueError: If the info is not in the expected format.
|
||||
"""
|
||||
keys_needed = set(("refresh_token", "client_id", "client_secret"))
|
||||
missing = keys_needed.difference(info.keys())
|
||||
|
||||
if missing:
|
||||
raise ValueError(
|
||||
"Authorized user info was not in the expected format, missing "
|
||||
"fields {}.".format(", ".join(missing))
|
||||
)
|
||||
|
||||
# access token expiry (datetime obj); auto-expire if not saved
|
||||
expiry = info.get("expiry")
|
||||
if expiry:
|
||||
expiry = datetime.strptime(
|
||||
expiry.rstrip("Z").split(".")[0], "%Y-%m-%dT%H:%M:%S"
|
||||
)
|
||||
else:
|
||||
expiry = _helpers.utcnow() - _helpers.REFRESH_THRESHOLD
|
||||
|
||||
# process scopes, which needs to be a seq
|
||||
if scopes is None and "scopes" in info:
|
||||
scopes = info.get("scopes")
|
||||
if isinstance(scopes, str):
|
||||
scopes = scopes.split(" ")
|
||||
|
||||
return cls(
|
||||
token=info.get("token"),
|
||||
refresh_token=info.get("refresh_token"),
|
||||
token_uri=_GOOGLE_OAUTH2_TOKEN_ENDPOINT, # always overrides
|
||||
scopes=scopes,
|
||||
client_id=info.get("client_id"),
|
||||
client_secret=info.get("client_secret"),
|
||||
quota_project_id=info.get("quota_project_id"), # may not exist
|
||||
expiry=expiry,
|
||||
rapt_token=info.get("rapt_token"), # may not exist
|
||||
trust_boundary=info.get("trust_boundary"), # may not exist
|
||||
universe_domain=info.get("universe_domain"), # may not exist
|
||||
account=info.get("account", ""), # may not exist
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_authorized_user_file(cls, filename, scopes=None):
|
||||
"""Creates a Credentials instance from an authorized user json file.
|
||||
|
||||
Args:
|
||||
filename (str): The path to the authorized user json file.
|
||||
scopes (Sequence[str]): Optional list of scopes to include in the
|
||||
credentials.
|
||||
|
||||
Returns:
|
||||
google.oauth2.credentials.Credentials: The constructed
|
||||
credentials.
|
||||
|
||||
Raises:
|
||||
ValueError: If the file is not in the expected format.
|
||||
"""
|
||||
with io.open(filename, "r", encoding="utf-8") as json_file:
|
||||
data = json.load(json_file)
|
||||
return cls.from_authorized_user_info(data, scopes)
|
||||
|
||||
def to_json(self, strip=None):
|
||||
"""Utility function that creates a JSON representation of a Credentials
|
||||
object.
|
||||
|
||||
Args:
|
||||
strip (Sequence[str]): Optional list of members to exclude from the
|
||||
generated JSON.
|
||||
|
||||
Returns:
|
||||
str: A JSON representation of this instance. When converted into
|
||||
a dictionary, it can be passed to from_authorized_user_info()
|
||||
to create a new credential instance.
|
||||
"""
|
||||
prep = {
|
||||
"token": self.token,
|
||||
"refresh_token": self.refresh_token,
|
||||
"token_uri": self.token_uri,
|
||||
"client_id": self.client_id,
|
||||
"client_secret": self.client_secret,
|
||||
"scopes": self.scopes,
|
||||
"rapt_token": self.rapt_token,
|
||||
"universe_domain": self._universe_domain,
|
||||
"account": self._account,
|
||||
}
|
||||
if self.expiry: # flatten expiry timestamp
|
||||
prep["expiry"] = self.expiry.isoformat() + "Z"
|
||||
|
||||
# Remove empty entries (those which are None)
|
||||
prep = {k: v for k, v in prep.items() if v is not None}
|
||||
|
||||
# Remove entries that explicitely need to be removed
|
||||
if strip is not None:
|
||||
prep = {k: v for k, v in prep.items() if k not in strip}
|
||||
|
||||
return json.dumps(prep)
|
||||
|
||||
|
||||
class UserAccessTokenCredentials(credentials.CredentialsWithQuotaProject):
|
||||
"""Access token credentials for user account.
|
||||
|
||||
Obtain the access token for a given user account or the current active
|
||||
user account with the ``gcloud auth print-access-token`` command.
|
||||
|
||||
Args:
|
||||
account (Optional[str]): Account to get the access token for. If not
|
||||
specified, the current active account will be used.
|
||||
quota_project_id (Optional[str]): The project ID used for quota
|
||||
and billing.
|
||||
"""
|
||||
|
||||
def __init__(self, account=None, quota_project_id=None):
|
||||
warnings.warn(
|
||||
"UserAccessTokenCredentials is deprecated, please use "
|
||||
"google.oauth2.credentials.Credentials instead. To use "
|
||||
"that credential type, simply run "
|
||||
"`gcloud auth application-default login` and let the "
|
||||
"client libraries pick up the application default credentials."
|
||||
)
|
||||
super(UserAccessTokenCredentials, self).__init__()
|
||||
self._account = account
|
||||
self._quota_project_id = quota_project_id
|
||||
|
||||
def with_account(self, account):
|
||||
"""Create a new instance with the given account.
|
||||
|
||||
Args:
|
||||
account (str): Account to get the access token for.
|
||||
|
||||
Returns:
|
||||
google.oauth2.credentials.UserAccessTokenCredentials: The created
|
||||
credentials with the given account.
|
||||
"""
|
||||
return self.__class__(account=account, quota_project_id=self._quota_project_id)
|
||||
|
||||
@_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
|
||||
def with_quota_project(self, quota_project_id):
|
||||
return self.__class__(account=self._account, quota_project_id=quota_project_id)
|
||||
|
||||
def refresh(self, request):
|
||||
"""Refreshes the access token.
|
||||
|
||||
Args:
|
||||
request (google.auth.transport.Request): This argument is required
|
||||
by the base class interface but not used in this implementation,
|
||||
so just set it to `None`.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.UserAccessTokenError: If the access token
|
||||
refresh failed.
|
||||
"""
|
||||
self.token = _cloud_sdk.get_auth_access_token(self._account)
|
||||
|
||||
@_helpers.copy_docstring(credentials.Credentials)
|
||||
def before_request(self, request, method, url, headers):
|
||||
self.refresh(request)
|
||||
self.apply(headers)
|
||||
@@ -0,0 +1,251 @@
|
||||
# Copyright 2022 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Experimental GDCH credentials support.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
|
||||
from google.auth import _helpers
|
||||
from google.auth import _service_account_info
|
||||
from google.auth import credentials
|
||||
from google.auth import exceptions
|
||||
from google.auth import jwt
|
||||
from google.oauth2 import _client
|
||||
|
||||
|
||||
TOKEN_EXCHANGE_TYPE = "urn:ietf:params:oauth:token-type:token-exchange"
|
||||
ACCESS_TOKEN_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token"
|
||||
SERVICE_ACCOUNT_TOKEN_TYPE = "urn:k8s:params:oauth:token-type:serviceaccount"
|
||||
JWT_LIFETIME = datetime.timedelta(seconds=3600) # 1 hour
|
||||
|
||||
|
||||
class ServiceAccountCredentials(credentials.Credentials):
|
||||
"""Credentials for GDCH (`Google Distributed Cloud Hosted`_) for service
|
||||
account users.
|
||||
|
||||
.. _Google Distributed Cloud Hosted:
|
||||
https://cloud.google.com/blog/topics/hybrid-cloud/\
|
||||
announcing-google-distributed-cloud-edge-and-hosted
|
||||
|
||||
To create a GDCH service account credential, first create a JSON file of
|
||||
the following format::
|
||||
|
||||
{
|
||||
"type": "gdch_service_account",
|
||||
"format_version": "1",
|
||||
"project": "<project name>",
|
||||
"private_key_id": "<key id>",
|
||||
"private_key": "-----BEGIN EC PRIVATE KEY-----\n<key bytes>\n-----END EC PRIVATE KEY-----\n",
|
||||
"name": "<service identity name>",
|
||||
"ca_cert_path": "<CA cert path>",
|
||||
"token_uri": "https://service-identity.<Domain>/authenticate"
|
||||
}
|
||||
|
||||
The "format_version" field stands for the format of the JSON file. For now
|
||||
it is always "1". The `private_key_id` and `private_key` is used for signing.
|
||||
The `ca_cert_path` is used for token server TLS certificate verification.
|
||||
|
||||
After the JSON file is created, set `GOOGLE_APPLICATION_CREDENTIALS` environment
|
||||
variable to the JSON file path, then use the following code to create the
|
||||
credential::
|
||||
|
||||
import google.auth
|
||||
|
||||
credential, _ = google.auth.default()
|
||||
credential = credential.with_gdch_audience("<the audience>")
|
||||
|
||||
We can also create the credential directly::
|
||||
|
||||
from google.oauth import gdch_credentials
|
||||
|
||||
credential = gdch_credentials.ServiceAccountCredentials.from_service_account_file("<the json file path>")
|
||||
credential = credential.with_gdch_audience("<the audience>")
|
||||
|
||||
The token is obtained in the following way. This class first creates a
|
||||
self signed JWT. It uses the `name` value as the `iss` and `sub` claim, and
|
||||
the `token_uri` as the `aud` claim, and signs the JWT with the `private_key`.
|
||||
It then sends the JWT to the `token_uri` to exchange a final token for
|
||||
`audience`.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, signer, service_identity_name, project, audience, token_uri, ca_cert_path
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
signer (google.auth.crypt.Signer): The signer used to sign JWTs.
|
||||
service_identity_name (str): The service identity name. It will be
|
||||
used as the `iss` and `sub` claim in the self signed JWT.
|
||||
project (str): The project.
|
||||
audience (str): The audience for the final token.
|
||||
token_uri (str): The token server uri.
|
||||
ca_cert_path (str): The CA cert path for token server side TLS
|
||||
certificate verification. If the token server uses well known
|
||||
CA, then this parameter can be `None`.
|
||||
"""
|
||||
super(ServiceAccountCredentials, self).__init__()
|
||||
self._signer = signer
|
||||
self._service_identity_name = service_identity_name
|
||||
self._project = project
|
||||
self._audience = audience
|
||||
self._token_uri = token_uri
|
||||
self._ca_cert_path = ca_cert_path
|
||||
|
||||
def _create_jwt(self):
|
||||
now = _helpers.utcnow()
|
||||
expiry = now + JWT_LIFETIME
|
||||
iss_sub_value = "system:serviceaccount:{}:{}".format(
|
||||
self._project, self._service_identity_name
|
||||
)
|
||||
|
||||
payload = {
|
||||
"iss": iss_sub_value,
|
||||
"sub": iss_sub_value,
|
||||
"aud": self._token_uri,
|
||||
"iat": _helpers.datetime_to_secs(now),
|
||||
"exp": _helpers.datetime_to_secs(expiry),
|
||||
}
|
||||
|
||||
return _helpers.from_bytes(jwt.encode(self._signer, payload))
|
||||
|
||||
@_helpers.copy_docstring(credentials.Credentials)
|
||||
def refresh(self, request):
|
||||
import google.auth.transport.requests
|
||||
|
||||
if not isinstance(request, google.auth.transport.requests.Request):
|
||||
raise exceptions.RefreshError(
|
||||
"For GDCH service account credentials, request must be a google.auth.transport.requests.Request object"
|
||||
)
|
||||
|
||||
# Create a self signed JWT, and do token exchange.
|
||||
jwt_token = self._create_jwt()
|
||||
request_body = {
|
||||
"grant_type": TOKEN_EXCHANGE_TYPE,
|
||||
"audience": self._audience,
|
||||
"requested_token_type": ACCESS_TOKEN_TOKEN_TYPE,
|
||||
"subject_token": jwt_token,
|
||||
"subject_token_type": SERVICE_ACCOUNT_TOKEN_TYPE,
|
||||
}
|
||||
response_data = _client._token_endpoint_request(
|
||||
request,
|
||||
self._token_uri,
|
||||
request_body,
|
||||
access_token=None,
|
||||
use_json=True,
|
||||
verify=self._ca_cert_path,
|
||||
)
|
||||
|
||||
self.token, _, self.expiry, _ = _client._handle_refresh_grant_response(
|
||||
response_data, None
|
||||
)
|
||||
|
||||
def with_gdch_audience(self, audience):
|
||||
"""Create a copy of GDCH credentials with the specified audience.
|
||||
|
||||
Args:
|
||||
audience (str): The intended audience for GDCH credentials.
|
||||
"""
|
||||
return self.__class__(
|
||||
self._signer,
|
||||
self._service_identity_name,
|
||||
self._project,
|
||||
audience,
|
||||
self._token_uri,
|
||||
self._ca_cert_path,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _from_signer_and_info(cls, signer, info):
|
||||
"""Creates a Credentials instance from a signer and service account
|
||||
info.
|
||||
|
||||
Args:
|
||||
signer (google.auth.crypt.Signer): The signer used to sign JWTs.
|
||||
info (Mapping[str, str]): The service account info.
|
||||
|
||||
Returns:
|
||||
google.oauth2.gdch_credentials.ServiceAccountCredentials: The constructed
|
||||
credentials.
|
||||
|
||||
Raises:
|
||||
ValueError: If the info is not in the expected format.
|
||||
"""
|
||||
if info["format_version"] != "1":
|
||||
raise ValueError("Only format version 1 is supported")
|
||||
|
||||
return cls(
|
||||
signer,
|
||||
info["name"], # service_identity_name
|
||||
info["project"],
|
||||
None, # audience
|
||||
info["token_uri"],
|
||||
info.get("ca_cert_path", None),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_service_account_info(cls, info):
|
||||
"""Creates a Credentials instance from parsed service account info.
|
||||
|
||||
Args:
|
||||
info (Mapping[str, str]): The service account info in Google
|
||||
format.
|
||||
kwargs: Additional arguments to pass to the constructor.
|
||||
|
||||
Returns:
|
||||
google.oauth2.gdch_credentials.ServiceAccountCredentials: The constructed
|
||||
credentials.
|
||||
|
||||
Raises:
|
||||
ValueError: If the info is not in the expected format.
|
||||
"""
|
||||
signer = _service_account_info.from_dict(
|
||||
info,
|
||||
require=[
|
||||
"format_version",
|
||||
"private_key_id",
|
||||
"private_key",
|
||||
"name",
|
||||
"project",
|
||||
"token_uri",
|
||||
],
|
||||
use_rsa_signer=False,
|
||||
)
|
||||
return cls._from_signer_and_info(signer, info)
|
||||
|
||||
@classmethod
|
||||
def from_service_account_file(cls, filename):
|
||||
"""Creates a Credentials instance from a service account json file.
|
||||
|
||||
Args:
|
||||
filename (str): The path to the service account json file.
|
||||
kwargs: Additional arguments to pass to the constructor.
|
||||
|
||||
Returns:
|
||||
google.oauth2.gdch_credentials.ServiceAccountCredentials: The constructed
|
||||
credentials.
|
||||
"""
|
||||
info, signer = _service_account_info.from_filename(
|
||||
filename,
|
||||
require=[
|
||||
"format_version",
|
||||
"private_key_id",
|
||||
"private_key",
|
||||
"name",
|
||||
"project",
|
||||
"token_uri",
|
||||
],
|
||||
use_rsa_signer=False,
|
||||
)
|
||||
return cls._from_signer_and_info(signer, info)
|
||||
370
.venv/lib/python3.10/site-packages/google/oauth2/id_token.py
Normal file
370
.venv/lib/python3.10/site-packages/google/oauth2/id_token.py
Normal file
@@ -0,0 +1,370 @@
|
||||
# Copyright 2016 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Google ID Token helpers.
|
||||
|
||||
Provides support for verifying `OpenID Connect ID Tokens`_, especially ones
|
||||
generated by Google infrastructure.
|
||||
|
||||
To parse and verify an ID Token issued by Google's OAuth 2.0 authorization
|
||||
server use :func:`verify_oauth2_token`. To verify an ID Token issued by
|
||||
Firebase, use :func:`verify_firebase_token`.
|
||||
|
||||
A general purpose ID Token verifier is available as :func:`verify_token`.
|
||||
|
||||
Example::
|
||||
|
||||
from google.oauth2 import id_token
|
||||
from google.auth.transport import requests
|
||||
|
||||
request = requests.Request()
|
||||
|
||||
id_info = id_token.verify_oauth2_token(
|
||||
token, request, 'my-client-id.example.com')
|
||||
|
||||
userid = id_info['sub']
|
||||
|
||||
By default, this will re-fetch certificates for each verification. Because
|
||||
Google's public keys are only changed infrequently (on the order of once per
|
||||
day), you may wish to take advantage of caching to reduce latency and the
|
||||
potential for network errors. This can be accomplished using an external
|
||||
library like `CacheControl`_ to create a cache-aware
|
||||
:class:`google.auth.transport.Request`::
|
||||
|
||||
import cachecontrol
|
||||
import google.auth.transport.requests
|
||||
import requests
|
||||
|
||||
session = requests.session()
|
||||
cached_session = cachecontrol.CacheControl(session)
|
||||
request = google.auth.transport.requests.Request(session=cached_session)
|
||||
|
||||
.. _OpenID Connect ID Tokens:
|
||||
http://openid.net/specs/openid-connect-core-1_0.html#IDToken
|
||||
.. _CacheControl: https://cachecontrol.readthedocs.io
|
||||
"""
|
||||
|
||||
import http.client as http_client
|
||||
import json
|
||||
import os
|
||||
|
||||
from google.auth import environment_vars
|
||||
from google.auth import exceptions
|
||||
from google.auth import jwt
|
||||
|
||||
|
||||
# The URL that provides public certificates for verifying ID tokens issued
|
||||
# by Google's OAuth 2.0 authorization server.
|
||||
_GOOGLE_OAUTH2_CERTS_URL = "https://www.googleapis.com/oauth2/v1/certs"
|
||||
|
||||
# The URL that provides public certificates for verifying ID tokens issued
|
||||
# by Firebase and the Google APIs infrastructure
|
||||
_GOOGLE_APIS_CERTS_URL = (
|
||||
"https://www.googleapis.com/robot/v1/metadata/x509"
|
||||
"/securetoken@system.gserviceaccount.com"
|
||||
)
|
||||
|
||||
_GOOGLE_ISSUERS = ["accounts.google.com", "https://accounts.google.com"]
|
||||
|
||||
|
||||
def _fetch_certs(request, certs_url):
|
||||
"""Fetches certificates.
|
||||
|
||||
Google-style cerificate endpoints return JSON in the format of
|
||||
``{'key id': 'x509 certificate'}`` or a certificate array according
|
||||
to the JWK spec (see https://tools.ietf.org/html/rfc7517).
|
||||
|
||||
Args:
|
||||
request (google.auth.transport.Request): The object used to make
|
||||
HTTP requests.
|
||||
certs_url (str): The certificate endpoint URL.
|
||||
|
||||
Returns:
|
||||
Mapping[str, str] | Mapping[str, list]: A mapping of public keys
|
||||
in x.509 or JWK spec.
|
||||
"""
|
||||
response = request(certs_url, method="GET")
|
||||
|
||||
if response.status != http_client.OK:
|
||||
raise exceptions.TransportError(
|
||||
"Could not fetch certificates at {}".format(certs_url)
|
||||
)
|
||||
|
||||
return json.loads(response.data.decode("utf-8"))
|
||||
|
||||
|
||||
def verify_token(
|
||||
id_token,
|
||||
request,
|
||||
audience=None,
|
||||
certs_url=_GOOGLE_OAUTH2_CERTS_URL,
|
||||
clock_skew_in_seconds=0,
|
||||
):
|
||||
"""Verifies an ID token and returns the decoded token.
|
||||
|
||||
Args:
|
||||
id_token (Union[str, bytes]): The encoded token.
|
||||
request (google.auth.transport.Request): The object used to make
|
||||
HTTP requests.
|
||||
audience (str or list): The audience or audiences that this token is
|
||||
intended for. If None then the audience is not verified.
|
||||
certs_url (str): The URL that specifies the certificates to use to
|
||||
verify the token. This URL should return JSON in the format of
|
||||
``{'key id': 'x509 certificate'}`` or a certificate array according to
|
||||
the JWK spec (see https://tools.ietf.org/html/rfc7517).
|
||||
clock_skew_in_seconds (int): The clock skew used for `iat` and `exp`
|
||||
validation.
|
||||
|
||||
Returns:
|
||||
Mapping[str, Any]: The decoded token.
|
||||
"""
|
||||
certs = _fetch_certs(request, certs_url)
|
||||
|
||||
if "keys" in certs:
|
||||
try:
|
||||
import jwt as jwt_lib # type: ignore
|
||||
except ImportError as caught_exc: # pragma: NO COVER
|
||||
raise ImportError(
|
||||
"The pyjwt library is not installed, please install the pyjwt package to use the jwk certs format."
|
||||
) from caught_exc
|
||||
jwks_client = jwt_lib.PyJWKClient(certs_url)
|
||||
signing_key = jwks_client.get_signing_key_from_jwt(id_token)
|
||||
return jwt_lib.decode(
|
||||
id_token,
|
||||
signing_key.key,
|
||||
algorithms=[signing_key.algorithm_name],
|
||||
audience=audience,
|
||||
)
|
||||
else:
|
||||
return jwt.decode(
|
||||
id_token,
|
||||
certs=certs,
|
||||
audience=audience,
|
||||
clock_skew_in_seconds=clock_skew_in_seconds,
|
||||
)
|
||||
|
||||
|
||||
def verify_oauth2_token(id_token, request, audience=None, clock_skew_in_seconds=0):
|
||||
"""Verifies an ID Token issued by Google's OAuth 2.0 authorization server.
|
||||
|
||||
Args:
|
||||
id_token (Union[str, bytes]): The encoded token.
|
||||
request (google.auth.transport.Request): The object used to make
|
||||
HTTP requests.
|
||||
audience (str): The audience that this token is intended for. This is
|
||||
typically your application's OAuth 2.0 client ID. If None then the
|
||||
audience is not verified.
|
||||
clock_skew_in_seconds (int): The clock skew used for `iat` and `exp`
|
||||
validation.
|
||||
|
||||
Returns:
|
||||
Mapping[str, Any]: The decoded token.
|
||||
|
||||
Raises:
|
||||
exceptions.GoogleAuthError: If the issuer is invalid.
|
||||
ValueError: If token verification fails
|
||||
"""
|
||||
idinfo = verify_token(
|
||||
id_token,
|
||||
request,
|
||||
audience=audience,
|
||||
certs_url=_GOOGLE_OAUTH2_CERTS_URL,
|
||||
clock_skew_in_seconds=clock_skew_in_seconds,
|
||||
)
|
||||
|
||||
if idinfo["iss"] not in _GOOGLE_ISSUERS:
|
||||
raise exceptions.GoogleAuthError(
|
||||
"Wrong issuer. 'iss' should be one of the following: {}".format(
|
||||
_GOOGLE_ISSUERS
|
||||
)
|
||||
)
|
||||
|
||||
return idinfo
|
||||
|
||||
|
||||
def verify_firebase_token(id_token, request, audience=None, clock_skew_in_seconds=0):
|
||||
"""Verifies an ID Token issued by Firebase Authentication.
|
||||
|
||||
Args:
|
||||
id_token (Union[str, bytes]): The encoded token.
|
||||
request (google.auth.transport.Request): The object used to make
|
||||
HTTP requests.
|
||||
audience (str): The audience that this token is intended for. This is
|
||||
typically your Firebase application ID. If None then the audience
|
||||
is not verified.
|
||||
clock_skew_in_seconds (int): The clock skew used for `iat` and `exp`
|
||||
validation.
|
||||
|
||||
Returns:
|
||||
Mapping[str, Any]: The decoded token.
|
||||
"""
|
||||
return verify_token(
|
||||
id_token,
|
||||
request,
|
||||
audience=audience,
|
||||
certs_url=_GOOGLE_APIS_CERTS_URL,
|
||||
clock_skew_in_seconds=clock_skew_in_seconds,
|
||||
)
|
||||
|
||||
|
||||
def fetch_id_token_credentials(audience, request=None):
|
||||
"""Create the ID Token credentials from the current environment.
|
||||
|
||||
This function acquires ID token from the environment in the following order.
|
||||
See https://google.aip.dev/auth/4110.
|
||||
|
||||
1. If the environment variable ``GOOGLE_APPLICATION_CREDENTIALS`` is set
|
||||
to the path of a valid service account JSON file, then ID token is
|
||||
acquired using this service account credentials.
|
||||
2. If the application is running in Compute Engine, App Engine or Cloud Run,
|
||||
then the ID token are obtained from the metadata server.
|
||||
3. If metadata server doesn't exist and no valid service account credentials
|
||||
are found, :class:`~google.auth.exceptions.DefaultCredentialsError` will
|
||||
be raised.
|
||||
|
||||
Example::
|
||||
|
||||
import google.oauth2.id_token
|
||||
import google.auth.transport.requests
|
||||
|
||||
request = google.auth.transport.requests.Request()
|
||||
target_audience = "https://pubsub.googleapis.com"
|
||||
|
||||
# Create ID token credentials.
|
||||
credentials = google.oauth2.id_token.fetch_id_token_credentials(target_audience, request=request)
|
||||
|
||||
# Refresh the credential to obtain an ID token.
|
||||
credentials.refresh(request)
|
||||
|
||||
id_token = credentials.token
|
||||
id_token_expiry = credentials.expiry
|
||||
|
||||
Args:
|
||||
audience (str): The audience that this ID token is intended for.
|
||||
request (Optional[google.auth.transport.Request]): A callable used to make
|
||||
HTTP requests. A request object will be created if not provided.
|
||||
|
||||
Returns:
|
||||
google.auth.credentials.Credentials: The ID token credentials.
|
||||
|
||||
Raises:
|
||||
~google.auth.exceptions.DefaultCredentialsError:
|
||||
If metadata server doesn't exist and no valid service account
|
||||
credentials are found.
|
||||
"""
|
||||
# 1. Try to get credentials from the GOOGLE_APPLICATION_CREDENTIALS environment
|
||||
# variable.
|
||||
credentials_filename = os.environ.get(environment_vars.CREDENTIALS)
|
||||
if credentials_filename:
|
||||
if not (
|
||||
os.path.exists(credentials_filename)
|
||||
and os.path.isfile(credentials_filename)
|
||||
):
|
||||
raise exceptions.DefaultCredentialsError(
|
||||
"GOOGLE_APPLICATION_CREDENTIALS path is either not found or invalid."
|
||||
)
|
||||
|
||||
try:
|
||||
with open(credentials_filename, "r") as f:
|
||||
from google.oauth2 import service_account
|
||||
|
||||
info = json.load(f)
|
||||
if info.get("type") == "service_account":
|
||||
return service_account.IDTokenCredentials.from_service_account_info(
|
||||
info, target_audience=audience
|
||||
)
|
||||
elif info.get("type") == "impersonated_service_account":
|
||||
from google.auth import impersonated_credentials
|
||||
|
||||
target_credentials = impersonated_credentials.Credentials.from_impersonated_service_account_info(
|
||||
info
|
||||
)
|
||||
|
||||
return impersonated_credentials.IDTokenCredentials(
|
||||
target_credentials=target_credentials,
|
||||
target_audience=audience,
|
||||
include_email=True,
|
||||
)
|
||||
except ValueError as caught_exc:
|
||||
new_exc = exceptions.DefaultCredentialsError(
|
||||
"GOOGLE_APPLICATION_CREDENTIALS is not valid service account credentials.",
|
||||
caught_exc,
|
||||
)
|
||||
raise new_exc from caught_exc
|
||||
|
||||
# 2. Try to fetch ID token from metada server if it exists. The code
|
||||
# works for GAE and Cloud Run metadata server as well.
|
||||
try:
|
||||
from google.auth import compute_engine
|
||||
from google.auth.compute_engine import _metadata
|
||||
|
||||
# Create a request object if not provided.
|
||||
if not request:
|
||||
import google.auth.transport.requests
|
||||
|
||||
request = google.auth.transport.requests.Request()
|
||||
|
||||
if _metadata.ping(request):
|
||||
return compute_engine.IDTokenCredentials(
|
||||
request, audience, use_metadata_identity_endpoint=True
|
||||
)
|
||||
except (ImportError, exceptions.TransportError):
|
||||
pass
|
||||
|
||||
raise exceptions.DefaultCredentialsError(
|
||||
"Neither metadata server or valid service account credentials are found."
|
||||
)
|
||||
|
||||
|
||||
def fetch_id_token(request, audience):
|
||||
"""Fetch the ID Token from the current environment.
|
||||
|
||||
This function acquires ID token from the environment in the following order.
|
||||
See https://google.aip.dev/auth/4110.
|
||||
|
||||
1. If the environment variable ``GOOGLE_APPLICATION_CREDENTIALS`` is set
|
||||
to the path of a valid service account JSON file, then ID token is
|
||||
acquired using this service account credentials.
|
||||
2. If the application is running in Compute Engine, App Engine or Cloud Run,
|
||||
then the ID token are obtained from the metadata server.
|
||||
3. If metadata server doesn't exist and no valid service account credentials
|
||||
are found, :class:`~google.auth.exceptions.DefaultCredentialsError` will
|
||||
be raised.
|
||||
|
||||
Example::
|
||||
|
||||
import google.oauth2.id_token
|
||||
import google.auth.transport.requests
|
||||
|
||||
request = google.auth.transport.requests.Request()
|
||||
target_audience = "https://pubsub.googleapis.com"
|
||||
|
||||
id_token = google.oauth2.id_token.fetch_id_token(request, target_audience)
|
||||
|
||||
Args:
|
||||
request (google.auth.transport.Request): A callable used to make
|
||||
HTTP requests.
|
||||
audience (str): The audience that this ID token is intended for.
|
||||
|
||||
Returns:
|
||||
str: The ID token.
|
||||
|
||||
Raises:
|
||||
~google.auth.exceptions.DefaultCredentialsError:
|
||||
If metadata server doesn't exist and no valid service account
|
||||
credentials are found.
|
||||
"""
|
||||
id_token_credentials = fetch_id_token_credentials(audience, request=request)
|
||||
id_token_credentials.refresh(request)
|
||||
return id_token_credentials.token
|
||||
@@ -0,0 +1,2 @@
|
||||
# Marker file for PEP 561.
|
||||
# The google-oauth2 package uses inline types.
|
||||
369
.venv/lib/python3.10/site-packages/google/oauth2/reauth.py
Normal file
369
.venv/lib/python3.10/site-packages/google/oauth2/reauth.py
Normal file
@@ -0,0 +1,369 @@
|
||||
# Copyright 2021 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""A module that provides functions for handling rapt authentication.
|
||||
|
||||
Reauth is a process of obtaining additional authentication (such as password,
|
||||
security token, etc.) while refreshing OAuth 2.0 credentials for a user.
|
||||
|
||||
Credentials that use the Reauth flow must have the reauth scope,
|
||||
``https://www.googleapis.com/auth/accounts.reauth``.
|
||||
|
||||
This module provides a high-level function for executing the Reauth process,
|
||||
:func:`refresh_grant`, and lower-level helpers for doing the individual
|
||||
steps of the reauth process.
|
||||
|
||||
Those steps are:
|
||||
|
||||
1. Obtaining a list of challenges from the reauth server.
|
||||
2. Running through each challenge and sending the result back to the reauth
|
||||
server.
|
||||
3. Refreshing the access token using the returned rapt token.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
from google.auth import exceptions
|
||||
from google.auth import metrics
|
||||
from google.oauth2 import _client
|
||||
from google.oauth2 import challenges
|
||||
|
||||
|
||||
_REAUTH_SCOPE = "https://www.googleapis.com/auth/accounts.reauth"
|
||||
_REAUTH_API = "https://reauth.googleapis.com/v2/sessions"
|
||||
|
||||
_REAUTH_NEEDED_ERROR = "invalid_grant"
|
||||
_REAUTH_NEEDED_ERROR_INVALID_RAPT = "invalid_rapt"
|
||||
_REAUTH_NEEDED_ERROR_RAPT_REQUIRED = "rapt_required"
|
||||
|
||||
_AUTHENTICATED = "AUTHENTICATED"
|
||||
_CHALLENGE_REQUIRED = "CHALLENGE_REQUIRED"
|
||||
_CHALLENGE_PENDING = "CHALLENGE_PENDING"
|
||||
|
||||
|
||||
# Override this global variable to set custom max number of rounds of reauth
|
||||
# challenges should be run.
|
||||
RUN_CHALLENGE_RETRY_LIMIT = 5
|
||||
|
||||
|
||||
def is_interactive():
|
||||
"""Check if we are in an interractive environment.
|
||||
|
||||
Override this function with a different logic if you are using this library
|
||||
outside a CLI.
|
||||
|
||||
If the rapt token needs refreshing, the user needs to answer the challenges.
|
||||
If the user is not in an interractive environment, the challenges can not
|
||||
be answered and we just wait for timeout for no reason.
|
||||
|
||||
Returns:
|
||||
bool: True if is interactive environment, False otherwise.
|
||||
"""
|
||||
|
||||
return sys.stdin.isatty()
|
||||
|
||||
|
||||
def _get_challenges(
|
||||
request, supported_challenge_types, access_token, requested_scopes=None
|
||||
):
|
||||
"""Does initial request to reauth API to get the challenges.
|
||||
|
||||
Args:
|
||||
request (google.auth.transport.Request): A callable used to make
|
||||
HTTP requests.
|
||||
supported_challenge_types (Sequence[str]): list of challenge names
|
||||
supported by the manager.
|
||||
access_token (str): Access token with reauth scopes.
|
||||
requested_scopes (Optional(Sequence[str])): Authorized scopes for the credentials.
|
||||
|
||||
Returns:
|
||||
dict: The response from the reauth API.
|
||||
"""
|
||||
body = {"supportedChallengeTypes": supported_challenge_types}
|
||||
if requested_scopes:
|
||||
body["oauthScopesForDomainPolicyLookup"] = requested_scopes
|
||||
metrics_header = {metrics.API_CLIENT_HEADER: metrics.reauth_start()}
|
||||
|
||||
return _client._token_endpoint_request(
|
||||
request,
|
||||
_REAUTH_API + ":start",
|
||||
body,
|
||||
access_token=access_token,
|
||||
use_json=True,
|
||||
headers=metrics_header,
|
||||
)
|
||||
|
||||
|
||||
def _send_challenge_result(
|
||||
request, session_id, challenge_id, client_input, access_token
|
||||
):
|
||||
"""Attempt to refresh access token by sending next challenge result.
|
||||
|
||||
Args:
|
||||
request (google.auth.transport.Request): A callable used to make
|
||||
HTTP requests.
|
||||
session_id (str): session id returned by the initial reauth call.
|
||||
challenge_id (str): challenge id returned by the initial reauth call.
|
||||
client_input: dict with a challenge-specific client input. For example:
|
||||
``{'credential': password}`` for password challenge.
|
||||
access_token (str): Access token with reauth scopes.
|
||||
|
||||
Returns:
|
||||
dict: The response from the reauth API.
|
||||
"""
|
||||
body = {
|
||||
"sessionId": session_id,
|
||||
"challengeId": challenge_id,
|
||||
"action": "RESPOND",
|
||||
"proposalResponse": client_input,
|
||||
}
|
||||
metrics_header = {metrics.API_CLIENT_HEADER: metrics.reauth_continue()}
|
||||
|
||||
return _client._token_endpoint_request(
|
||||
request,
|
||||
_REAUTH_API + "/{}:continue".format(session_id),
|
||||
body,
|
||||
access_token=access_token,
|
||||
use_json=True,
|
||||
headers=metrics_header,
|
||||
)
|
||||
|
||||
|
||||
def _run_next_challenge(msg, request, access_token):
|
||||
"""Get the next challenge from msg and run it.
|
||||
|
||||
Args:
|
||||
msg (dict): Reauth API response body (either from the initial request to
|
||||
https://reauth.googleapis.com/v2/sessions:start or from sending the
|
||||
previous challenge response to
|
||||
https://reauth.googleapis.com/v2/sessions/id:continue)
|
||||
request (google.auth.transport.Request): A callable used to make
|
||||
HTTP requests.
|
||||
access_token (str): reauth access token
|
||||
|
||||
Returns:
|
||||
dict: The response from the reauth API.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.ReauthError: if reauth failed.
|
||||
"""
|
||||
for challenge in msg["challenges"]:
|
||||
if challenge["status"] != "READY":
|
||||
# Skip non-activated challenges.
|
||||
continue
|
||||
c = challenges.AVAILABLE_CHALLENGES.get(challenge["challengeType"], None)
|
||||
if not c:
|
||||
raise exceptions.ReauthFailError(
|
||||
"Unsupported challenge type {0}. Supported types: {1}".format(
|
||||
challenge["challengeType"],
|
||||
",".join(list(challenges.AVAILABLE_CHALLENGES.keys())),
|
||||
)
|
||||
)
|
||||
if not c.is_locally_eligible:
|
||||
raise exceptions.ReauthFailError(
|
||||
"Challenge {0} is not locally eligible".format(
|
||||
challenge["challengeType"]
|
||||
)
|
||||
)
|
||||
client_input = c.obtain_challenge_input(challenge)
|
||||
if not client_input:
|
||||
return None
|
||||
return _send_challenge_result(
|
||||
request,
|
||||
msg["sessionId"],
|
||||
challenge["challengeId"],
|
||||
client_input,
|
||||
access_token,
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def _obtain_rapt(request, access_token, requested_scopes):
|
||||
"""Given an http request method and reauth access token, get rapt token.
|
||||
|
||||
Args:
|
||||
request (google.auth.transport.Request): A callable used to make
|
||||
HTTP requests.
|
||||
access_token (str): reauth access token
|
||||
requested_scopes (Sequence[str]): scopes required by the client application
|
||||
|
||||
Returns:
|
||||
str: The rapt token.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.ReauthError: if reauth failed
|
||||
"""
|
||||
msg = _get_challenges(
|
||||
request,
|
||||
list(challenges.AVAILABLE_CHALLENGES.keys()),
|
||||
access_token,
|
||||
requested_scopes,
|
||||
)
|
||||
|
||||
if msg["status"] == _AUTHENTICATED:
|
||||
return msg["encodedProofOfReauthToken"]
|
||||
|
||||
for _ in range(0, RUN_CHALLENGE_RETRY_LIMIT):
|
||||
if not (
|
||||
msg["status"] == _CHALLENGE_REQUIRED or msg["status"] == _CHALLENGE_PENDING
|
||||
):
|
||||
raise exceptions.ReauthFailError(
|
||||
"Reauthentication challenge failed due to API error: {}".format(
|
||||
msg["status"]
|
||||
)
|
||||
)
|
||||
|
||||
if not is_interactive():
|
||||
raise exceptions.ReauthFailError(
|
||||
"Reauthentication challenge could not be answered because you are not"
|
||||
" in an interactive session."
|
||||
)
|
||||
|
||||
msg = _run_next_challenge(msg, request, access_token)
|
||||
|
||||
if not msg:
|
||||
raise exceptions.ReauthFailError("Failed to obtain rapt token.")
|
||||
if msg["status"] == _AUTHENTICATED:
|
||||
return msg["encodedProofOfReauthToken"]
|
||||
|
||||
# If we got here it means we didn't get authenticated.
|
||||
raise exceptions.ReauthFailError("Failed to obtain rapt token.")
|
||||
|
||||
|
||||
def get_rapt_token(
|
||||
request, client_id, client_secret, refresh_token, token_uri, scopes=None
|
||||
):
|
||||
"""Given an http request method and refresh_token, get rapt token.
|
||||
|
||||
Args:
|
||||
request (google.auth.transport.Request): A callable used to make
|
||||
HTTP requests.
|
||||
client_id (str): client id to get access token for reauth scope.
|
||||
client_secret (str): client secret for the client_id
|
||||
refresh_token (str): refresh token to refresh access token
|
||||
token_uri (str): uri to refresh access token
|
||||
scopes (Optional(Sequence[str])): scopes required by the client application
|
||||
|
||||
Returns:
|
||||
str: The rapt token.
|
||||
Raises:
|
||||
google.auth.exceptions.RefreshError: If reauth failed.
|
||||
"""
|
||||
sys.stderr.write("Reauthentication required.\n")
|
||||
|
||||
# Get access token for reauth.
|
||||
access_token, _, _, _ = _client.refresh_grant(
|
||||
request=request,
|
||||
client_id=client_id,
|
||||
client_secret=client_secret,
|
||||
refresh_token=refresh_token,
|
||||
token_uri=token_uri,
|
||||
scopes=[_REAUTH_SCOPE],
|
||||
)
|
||||
|
||||
# Get rapt token from reauth API.
|
||||
rapt_token = _obtain_rapt(request, access_token, requested_scopes=scopes)
|
||||
sys.stderr.write("Reauthentication successful.\n")
|
||||
|
||||
return rapt_token
|
||||
|
||||
|
||||
def refresh_grant(
|
||||
request,
|
||||
token_uri,
|
||||
refresh_token,
|
||||
client_id,
|
||||
client_secret,
|
||||
scopes=None,
|
||||
rapt_token=None,
|
||||
enable_reauth_refresh=False,
|
||||
):
|
||||
"""Implements the reauthentication flow.
|
||||
|
||||
Args:
|
||||
request (google.auth.transport.Request): A callable used to make
|
||||
HTTP requests.
|
||||
token_uri (str): The OAuth 2.0 authorizations server's token endpoint
|
||||
URI.
|
||||
refresh_token (str): The refresh token to use to get a new access
|
||||
token.
|
||||
client_id (str): The OAuth 2.0 application's client ID.
|
||||
client_secret (str): The Oauth 2.0 appliaction's client secret.
|
||||
scopes (Optional(Sequence[str])): Scopes to request. If present, all
|
||||
scopes must be authorized for the refresh token. Useful if refresh
|
||||
token has a wild card scope (e.g.
|
||||
'https://www.googleapis.com/auth/any-api').
|
||||
rapt_token (Optional(str)): The rapt token for reauth.
|
||||
enable_reauth_refresh (Optional[bool]): Whether reauth refresh flow
|
||||
should be used. The default value is False. This option is for
|
||||
gcloud only, other users should use the default value.
|
||||
|
||||
Returns:
|
||||
Tuple[str, Optional[str], Optional[datetime], Mapping[str, str], str]: The
|
||||
access token, new refresh token, expiration, the additional data
|
||||
returned by the token endpoint, and the rapt token.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.RefreshError: If the token endpoint returned
|
||||
an error.
|
||||
"""
|
||||
body = {
|
||||
"grant_type": _client._REFRESH_GRANT_TYPE,
|
||||
"client_id": client_id,
|
||||
"client_secret": client_secret,
|
||||
"refresh_token": refresh_token,
|
||||
}
|
||||
if scopes:
|
||||
body["scope"] = " ".join(scopes)
|
||||
if rapt_token:
|
||||
body["rapt"] = rapt_token
|
||||
metrics_header = {metrics.API_CLIENT_HEADER: metrics.token_request_user()}
|
||||
|
||||
response_status_ok, response_data, retryable_error = _client._token_endpoint_request_no_throw(
|
||||
request, token_uri, body, headers=metrics_header
|
||||
)
|
||||
|
||||
if not response_status_ok and isinstance(response_data, str):
|
||||
raise exceptions.RefreshError(response_data, retryable=False)
|
||||
|
||||
if (
|
||||
not response_status_ok
|
||||
and response_data.get("error") == _REAUTH_NEEDED_ERROR
|
||||
and (
|
||||
response_data.get("error_subtype") == _REAUTH_NEEDED_ERROR_INVALID_RAPT
|
||||
or response_data.get("error_subtype") == _REAUTH_NEEDED_ERROR_RAPT_REQUIRED
|
||||
)
|
||||
):
|
||||
if not enable_reauth_refresh:
|
||||
raise exceptions.RefreshError(
|
||||
"Reauthentication is needed. Please run `gcloud auth application-default login` to reauthenticate."
|
||||
)
|
||||
|
||||
rapt_token = get_rapt_token(
|
||||
request, client_id, client_secret, refresh_token, token_uri, scopes=scopes
|
||||
)
|
||||
body["rapt"] = rapt_token
|
||||
(
|
||||
response_status_ok,
|
||||
response_data,
|
||||
retryable_error,
|
||||
) = _client._token_endpoint_request_no_throw(
|
||||
request, token_uri, body, headers=metrics_header
|
||||
)
|
||||
|
||||
if not response_status_ok:
|
||||
_client._handle_error_response(response_data, retryable_error)
|
||||
return _client._handle_refresh_grant_response(response_data, refresh_token) + (
|
||||
rapt_token,
|
||||
)
|
||||
@@ -0,0 +1,847 @@
|
||||
# Copyright 2016 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Service Accounts: JSON Web Token (JWT) Profile for OAuth 2.0
|
||||
|
||||
This module implements the JWT Profile for OAuth 2.0 Authorization Grants
|
||||
as defined by `RFC 7523`_ with particular support for how this RFC is
|
||||
implemented in Google's infrastructure. Google refers to these credentials
|
||||
as *Service Accounts*.
|
||||
|
||||
Service accounts are used for server-to-server communication, such as
|
||||
interactions between a web application server and a Google service. The
|
||||
service account belongs to your application instead of to an individual end
|
||||
user. In contrast to other OAuth 2.0 profiles, no users are involved and your
|
||||
application "acts" as the service account.
|
||||
|
||||
Typically an application uses a service account when the application uses
|
||||
Google APIs to work with its own data rather than a user's data. For example,
|
||||
an application that uses Google Cloud Datastore for data persistence would use
|
||||
a service account to authenticate its calls to the Google Cloud Datastore API.
|
||||
However, an application that needs to access a user's Drive documents would
|
||||
use the normal OAuth 2.0 profile.
|
||||
|
||||
Additionally, Google Apps domain administrators can grant service accounts
|
||||
`domain-wide delegation`_ authority to access user data on behalf of users in
|
||||
the domain.
|
||||
|
||||
This profile uses a JWT to acquire an OAuth 2.0 access token. The JWT is used
|
||||
in place of the usual authorization token returned during the standard
|
||||
OAuth 2.0 Authorization Code grant. The JWT is only used for this purpose, as
|
||||
the acquired access token is used as the bearer token when making requests
|
||||
using these credentials.
|
||||
|
||||
This profile differs from normal OAuth 2.0 profile because no user consent
|
||||
step is required. The use of the private key allows this profile to assert
|
||||
identity directly.
|
||||
|
||||
This profile also differs from the :mod:`google.auth.jwt` authentication
|
||||
because the JWT credentials use the JWT directly as the bearer token. This
|
||||
profile instead only uses the JWT to obtain an OAuth 2.0 access token. The
|
||||
obtained OAuth 2.0 access token is used as the bearer token.
|
||||
|
||||
Domain-wide delegation
|
||||
----------------------
|
||||
|
||||
Domain-wide delegation allows a service account to access user data on
|
||||
behalf of any user in a Google Apps domain without consent from the user.
|
||||
For example, an application that uses the Google Calendar API to add events to
|
||||
the calendars of all users in a Google Apps domain would use a service account
|
||||
to access the Google Calendar API on behalf of users.
|
||||
|
||||
The Google Apps administrator must explicitly authorize the service account to
|
||||
do this. This authorization step is referred to as "delegating domain-wide
|
||||
authority" to a service account.
|
||||
|
||||
You can use domain-wise delegation by creating a set of credentials with a
|
||||
specific subject using :meth:`~Credentials.with_subject`.
|
||||
|
||||
.. _RFC 7523: https://tools.ietf.org/html/rfc7523
|
||||
"""
|
||||
|
||||
import copy
|
||||
import datetime
|
||||
|
||||
from google.auth import _helpers
|
||||
from google.auth import _service_account_info
|
||||
from google.auth import credentials
|
||||
from google.auth import exceptions
|
||||
from google.auth import iam
|
||||
from google.auth import jwt
|
||||
from google.auth import metrics
|
||||
from google.oauth2 import _client
|
||||
|
||||
_DEFAULT_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
|
||||
_GOOGLE_OAUTH2_TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token"
|
||||
|
||||
|
||||
class Credentials(
|
||||
credentials.Signing,
|
||||
credentials.Scoped,
|
||||
credentials.CredentialsWithQuotaProject,
|
||||
credentials.CredentialsWithTokenUri,
|
||||
):
|
||||
"""Service account credentials
|
||||
|
||||
Usually, you'll create these credentials with one of the helper
|
||||
constructors. To create credentials using a Google service account
|
||||
private key JSON file::
|
||||
|
||||
credentials = service_account.Credentials.from_service_account_file(
|
||||
'service-account.json')
|
||||
|
||||
Or if you already have the service account file loaded::
|
||||
|
||||
service_account_info = json.load(open('service_account.json'))
|
||||
credentials = service_account.Credentials.from_service_account_info(
|
||||
service_account_info)
|
||||
|
||||
Both helper methods pass on arguments to the constructor, so you can
|
||||
specify additional scopes and a subject if necessary::
|
||||
|
||||
credentials = service_account.Credentials.from_service_account_file(
|
||||
'service-account.json',
|
||||
scopes=['email'],
|
||||
subject='user@example.com')
|
||||
|
||||
The credentials are considered immutable. If you want to modify the scopes
|
||||
or the subject used for delegation, use :meth:`with_scopes` or
|
||||
:meth:`with_subject`::
|
||||
|
||||
scoped_credentials = credentials.with_scopes(['email'])
|
||||
delegated_credentials = credentials.with_subject(subject)
|
||||
|
||||
To add a quota project, use :meth:`with_quota_project`::
|
||||
|
||||
credentials = credentials.with_quota_project('myproject-123')
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
signer,
|
||||
service_account_email,
|
||||
token_uri,
|
||||
scopes=None,
|
||||
default_scopes=None,
|
||||
subject=None,
|
||||
project_id=None,
|
||||
quota_project_id=None,
|
||||
additional_claims=None,
|
||||
always_use_jwt_access=False,
|
||||
universe_domain=credentials.DEFAULT_UNIVERSE_DOMAIN,
|
||||
trust_boundary=None,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
signer (google.auth.crypt.Signer): The signer used to sign JWTs.
|
||||
service_account_email (str): The service account's email.
|
||||
scopes (Sequence[str]): User-defined scopes to request during the
|
||||
authorization grant.
|
||||
default_scopes (Sequence[str]): Default scopes passed by a
|
||||
Google client library. Use 'scopes' for user-defined scopes.
|
||||
token_uri (str): The OAuth 2.0 Token URI.
|
||||
subject (str): For domain-wide delegation, the email address of the
|
||||
user to for which to request delegated access.
|
||||
project_id (str): Project ID associated with the service account
|
||||
credential.
|
||||
quota_project_id (Optional[str]): The project ID used for quota and
|
||||
billing.
|
||||
additional_claims (Mapping[str, str]): Any additional claims for
|
||||
the JWT assertion used in the authorization grant.
|
||||
always_use_jwt_access (Optional[bool]): Whether self signed JWT should
|
||||
be always used.
|
||||
universe_domain (str): The universe domain. The default
|
||||
universe domain is googleapis.com. For default value self
|
||||
signed jwt is used for token refresh.
|
||||
trust_boundary (str): String representation of trust boundary meta.
|
||||
|
||||
.. note:: Typically one of the helper constructors
|
||||
:meth:`from_service_account_file` or
|
||||
:meth:`from_service_account_info` are used instead of calling the
|
||||
constructor directly.
|
||||
"""
|
||||
super(Credentials, self).__init__()
|
||||
|
||||
self._cred_file_path = None
|
||||
self._scopes = scopes
|
||||
self._default_scopes = default_scopes
|
||||
self._signer = signer
|
||||
self._service_account_email = service_account_email
|
||||
self._subject = subject
|
||||
self._project_id = project_id
|
||||
self._quota_project_id = quota_project_id
|
||||
self._token_uri = token_uri
|
||||
self._always_use_jwt_access = always_use_jwt_access
|
||||
self._universe_domain = universe_domain or credentials.DEFAULT_UNIVERSE_DOMAIN
|
||||
|
||||
if universe_domain != credentials.DEFAULT_UNIVERSE_DOMAIN:
|
||||
self._always_use_jwt_access = True
|
||||
|
||||
self._jwt_credentials = None
|
||||
|
||||
if additional_claims is not None:
|
||||
self._additional_claims = additional_claims
|
||||
else:
|
||||
self._additional_claims = {}
|
||||
self._trust_boundary = {"locations": [], "encoded_locations": "0x0"}
|
||||
|
||||
@classmethod
|
||||
def _from_signer_and_info(cls, signer, info, **kwargs):
|
||||
"""Creates a Credentials instance from a signer and service account
|
||||
info.
|
||||
|
||||
Args:
|
||||
signer (google.auth.crypt.Signer): The signer used to sign JWTs.
|
||||
info (Mapping[str, str]): The service account info.
|
||||
kwargs: Additional arguments to pass to the constructor.
|
||||
|
||||
Returns:
|
||||
google.auth.jwt.Credentials: The constructed credentials.
|
||||
|
||||
Raises:
|
||||
ValueError: If the info is not in the expected format.
|
||||
"""
|
||||
return cls(
|
||||
signer,
|
||||
service_account_email=info["client_email"],
|
||||
token_uri=info["token_uri"],
|
||||
project_id=info.get("project_id"),
|
||||
universe_domain=info.get(
|
||||
"universe_domain", credentials.DEFAULT_UNIVERSE_DOMAIN
|
||||
),
|
||||
trust_boundary=info.get("trust_boundary"),
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_service_account_info(cls, info, **kwargs):
|
||||
"""Creates a Credentials instance from parsed service account info.
|
||||
|
||||
Args:
|
||||
info (Mapping[str, str]): The service account info in Google
|
||||
format.
|
||||
kwargs: Additional arguments to pass to the constructor.
|
||||
|
||||
Returns:
|
||||
google.auth.service_account.Credentials: The constructed
|
||||
credentials.
|
||||
|
||||
Raises:
|
||||
ValueError: If the info is not in the expected format.
|
||||
"""
|
||||
signer = _service_account_info.from_dict(
|
||||
info, require=["client_email", "token_uri"]
|
||||
)
|
||||
return cls._from_signer_and_info(signer, info, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def from_service_account_file(cls, filename, **kwargs):
|
||||
"""Creates a Credentials instance from a service account json file.
|
||||
|
||||
Args:
|
||||
filename (str): The path to the service account json file.
|
||||
kwargs: Additional arguments to pass to the constructor.
|
||||
|
||||
Returns:
|
||||
google.auth.service_account.Credentials: The constructed
|
||||
credentials.
|
||||
"""
|
||||
info, signer = _service_account_info.from_filename(
|
||||
filename, require=["client_email", "token_uri"]
|
||||
)
|
||||
return cls._from_signer_and_info(signer, info, **kwargs)
|
||||
|
||||
@property
|
||||
def service_account_email(self):
|
||||
"""The service account email."""
|
||||
return self._service_account_email
|
||||
|
||||
@property
|
||||
def project_id(self):
|
||||
"""Project ID associated with this credential."""
|
||||
return self._project_id
|
||||
|
||||
@property
|
||||
def requires_scopes(self):
|
||||
"""Checks if the credentials requires scopes.
|
||||
|
||||
Returns:
|
||||
bool: True if there are no scopes set otherwise False.
|
||||
"""
|
||||
return True if not self._scopes else False
|
||||
|
||||
def _make_copy(self):
|
||||
cred = self.__class__(
|
||||
self._signer,
|
||||
service_account_email=self._service_account_email,
|
||||
scopes=copy.copy(self._scopes),
|
||||
default_scopes=copy.copy(self._default_scopes),
|
||||
token_uri=self._token_uri,
|
||||
subject=self._subject,
|
||||
project_id=self._project_id,
|
||||
quota_project_id=self._quota_project_id,
|
||||
additional_claims=self._additional_claims.copy(),
|
||||
always_use_jwt_access=self._always_use_jwt_access,
|
||||
universe_domain=self._universe_domain,
|
||||
)
|
||||
cred._cred_file_path = self._cred_file_path
|
||||
return cred
|
||||
|
||||
@_helpers.copy_docstring(credentials.Scoped)
|
||||
def with_scopes(self, scopes, default_scopes=None):
|
||||
cred = self._make_copy()
|
||||
cred._scopes = scopes
|
||||
cred._default_scopes = default_scopes
|
||||
return cred
|
||||
|
||||
def with_always_use_jwt_access(self, always_use_jwt_access):
|
||||
"""Create a copy of these credentials with the specified always_use_jwt_access value.
|
||||
|
||||
Args:
|
||||
always_use_jwt_access (bool): Whether always use self signed JWT or not.
|
||||
|
||||
Returns:
|
||||
google.auth.service_account.Credentials: A new credentials
|
||||
instance.
|
||||
Raises:
|
||||
google.auth.exceptions.InvalidValue: If the universe domain is not
|
||||
default and always_use_jwt_access is False.
|
||||
"""
|
||||
cred = self._make_copy()
|
||||
if (
|
||||
cred._universe_domain != credentials.DEFAULT_UNIVERSE_DOMAIN
|
||||
and not always_use_jwt_access
|
||||
):
|
||||
raise exceptions.InvalidValue(
|
||||
"always_use_jwt_access should be True for non-default universe domain"
|
||||
)
|
||||
cred._always_use_jwt_access = always_use_jwt_access
|
||||
return cred
|
||||
|
||||
@_helpers.copy_docstring(credentials.CredentialsWithUniverseDomain)
|
||||
def with_universe_domain(self, universe_domain):
|
||||
cred = self._make_copy()
|
||||
cred._universe_domain = universe_domain
|
||||
if universe_domain != credentials.DEFAULT_UNIVERSE_DOMAIN:
|
||||
cred._always_use_jwt_access = True
|
||||
return cred
|
||||
|
||||
def with_subject(self, subject):
|
||||
"""Create a copy of these credentials with the specified subject.
|
||||
|
||||
Args:
|
||||
subject (str): The subject claim.
|
||||
|
||||
Returns:
|
||||
google.auth.service_account.Credentials: A new credentials
|
||||
instance.
|
||||
"""
|
||||
cred = self._make_copy()
|
||||
cred._subject = subject
|
||||
return cred
|
||||
|
||||
def with_claims(self, additional_claims):
|
||||
"""Returns a copy of these credentials with modified claims.
|
||||
|
||||
Args:
|
||||
additional_claims (Mapping[str, str]): Any additional claims for
|
||||
the JWT payload. This will be merged with the current
|
||||
additional claims.
|
||||
|
||||
Returns:
|
||||
google.auth.service_account.Credentials: A new credentials
|
||||
instance.
|
||||
"""
|
||||
new_additional_claims = copy.deepcopy(self._additional_claims)
|
||||
new_additional_claims.update(additional_claims or {})
|
||||
cred = self._make_copy()
|
||||
cred._additional_claims = new_additional_claims
|
||||
return cred
|
||||
|
||||
@_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
|
||||
def with_quota_project(self, quota_project_id):
|
||||
cred = self._make_copy()
|
||||
cred._quota_project_id = quota_project_id
|
||||
return cred
|
||||
|
||||
@_helpers.copy_docstring(credentials.CredentialsWithTokenUri)
|
||||
def with_token_uri(self, token_uri):
|
||||
cred = self._make_copy()
|
||||
cred._token_uri = token_uri
|
||||
return cred
|
||||
|
||||
def _make_authorization_grant_assertion(self):
|
||||
"""Create the OAuth 2.0 assertion.
|
||||
|
||||
This assertion is used during the OAuth 2.0 grant to acquire an
|
||||
access token.
|
||||
|
||||
Returns:
|
||||
bytes: The authorization grant assertion.
|
||||
"""
|
||||
now = _helpers.utcnow()
|
||||
lifetime = datetime.timedelta(seconds=_DEFAULT_TOKEN_LIFETIME_SECS)
|
||||
expiry = now + lifetime
|
||||
|
||||
payload = {
|
||||
"iat": _helpers.datetime_to_secs(now),
|
||||
"exp": _helpers.datetime_to_secs(expiry),
|
||||
# The issuer must be the service account email.
|
||||
"iss": self._service_account_email,
|
||||
# The audience must be the auth token endpoint's URI
|
||||
"aud": _GOOGLE_OAUTH2_TOKEN_ENDPOINT,
|
||||
"scope": _helpers.scopes_to_string(self._scopes or ()),
|
||||
}
|
||||
|
||||
payload.update(self._additional_claims)
|
||||
|
||||
# The subject can be a user email for domain-wide delegation.
|
||||
if self._subject:
|
||||
payload.setdefault("sub", self._subject)
|
||||
|
||||
token = jwt.encode(self._signer, payload)
|
||||
|
||||
return token
|
||||
|
||||
def _use_self_signed_jwt(self):
|
||||
# Since domain wide delegation doesn't work with self signed JWT. If
|
||||
# subject exists, then we should not use self signed JWT.
|
||||
return self._subject is None and self._jwt_credentials is not None
|
||||
|
||||
def _metric_header_for_usage(self):
|
||||
if self._use_self_signed_jwt():
|
||||
return metrics.CRED_TYPE_SA_JWT
|
||||
return metrics.CRED_TYPE_SA_ASSERTION
|
||||
|
||||
@_helpers.copy_docstring(credentials.Credentials)
|
||||
def refresh(self, request):
|
||||
if self._always_use_jwt_access and not self._jwt_credentials:
|
||||
# If self signed jwt should be used but jwt credential is not
|
||||
# created, try to create one with scopes
|
||||
self._create_self_signed_jwt(None)
|
||||
|
||||
if (
|
||||
self._universe_domain != credentials.DEFAULT_UNIVERSE_DOMAIN
|
||||
and self._subject
|
||||
):
|
||||
raise exceptions.RefreshError(
|
||||
"domain wide delegation is not supported for non-default universe domain"
|
||||
)
|
||||
|
||||
if self._use_self_signed_jwt():
|
||||
self._jwt_credentials.refresh(request)
|
||||
self.token = self._jwt_credentials.token.decode()
|
||||
self.expiry = self._jwt_credentials.expiry
|
||||
else:
|
||||
assertion = self._make_authorization_grant_assertion()
|
||||
access_token, expiry, _ = _client.jwt_grant(
|
||||
request, self._token_uri, assertion
|
||||
)
|
||||
self.token = access_token
|
||||
self.expiry = expiry
|
||||
|
||||
def _create_self_signed_jwt(self, audience):
|
||||
"""Create a self-signed JWT from the credentials if requirements are met.
|
||||
|
||||
Args:
|
||||
audience (str): The service URL. ``https://[API_ENDPOINT]/``
|
||||
"""
|
||||
# https://google.aip.dev/auth/4111
|
||||
if self._always_use_jwt_access:
|
||||
if self._scopes:
|
||||
additional_claims = {"scope": " ".join(self._scopes)}
|
||||
if (
|
||||
self._jwt_credentials is None
|
||||
or self._jwt_credentials.additional_claims != additional_claims
|
||||
):
|
||||
self._jwt_credentials = jwt.Credentials.from_signing_credentials(
|
||||
self, None, additional_claims=additional_claims
|
||||
)
|
||||
elif audience:
|
||||
if (
|
||||
self._jwt_credentials is None
|
||||
or self._jwt_credentials._audience != audience
|
||||
):
|
||||
|
||||
self._jwt_credentials = jwt.Credentials.from_signing_credentials(
|
||||
self, audience
|
||||
)
|
||||
elif self._default_scopes:
|
||||
additional_claims = {"scope": " ".join(self._default_scopes)}
|
||||
if (
|
||||
self._jwt_credentials is None
|
||||
or additional_claims != self._jwt_credentials.additional_claims
|
||||
):
|
||||
self._jwt_credentials = jwt.Credentials.from_signing_credentials(
|
||||
self, None, additional_claims=additional_claims
|
||||
)
|
||||
elif not self._scopes and audience:
|
||||
self._jwt_credentials = jwt.Credentials.from_signing_credentials(
|
||||
self, audience
|
||||
)
|
||||
|
||||
@_helpers.copy_docstring(credentials.Signing)
|
||||
def sign_bytes(self, message):
|
||||
return self._signer.sign(message)
|
||||
|
||||
@property # type: ignore
|
||||
@_helpers.copy_docstring(credentials.Signing)
|
||||
def signer(self):
|
||||
return self._signer
|
||||
|
||||
@property # type: ignore
|
||||
@_helpers.copy_docstring(credentials.Signing)
|
||||
def signer_email(self):
|
||||
return self._service_account_email
|
||||
|
||||
@_helpers.copy_docstring(credentials.Credentials)
|
||||
def get_cred_info(self):
|
||||
if self._cred_file_path:
|
||||
return {
|
||||
"credential_source": self._cred_file_path,
|
||||
"credential_type": "service account credentials",
|
||||
"principal": self.service_account_email,
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
class IDTokenCredentials(
|
||||
credentials.Signing,
|
||||
credentials.CredentialsWithQuotaProject,
|
||||
credentials.CredentialsWithTokenUri,
|
||||
):
|
||||
"""Open ID Connect ID Token-based service account credentials.
|
||||
|
||||
These credentials are largely similar to :class:`.Credentials`, but instead
|
||||
of using an OAuth 2.0 Access Token as the bearer token, they use an Open
|
||||
ID Connect ID Token as the bearer token. These credentials are useful when
|
||||
communicating to services that require ID Tokens and can not accept access
|
||||
tokens.
|
||||
|
||||
Usually, you'll create these credentials with one of the helper
|
||||
constructors. To create credentials using a Google service account
|
||||
private key JSON file::
|
||||
|
||||
credentials = (
|
||||
service_account.IDTokenCredentials.from_service_account_file(
|
||||
'service-account.json'))
|
||||
|
||||
|
||||
Or if you already have the service account file loaded::
|
||||
|
||||
service_account_info = json.load(open('service_account.json'))
|
||||
credentials = (
|
||||
service_account.IDTokenCredentials.from_service_account_info(
|
||||
service_account_info))
|
||||
|
||||
|
||||
Both helper methods pass on arguments to the constructor, so you can
|
||||
specify additional scopes and a subject if necessary::
|
||||
|
||||
credentials = (
|
||||
service_account.IDTokenCredentials.from_service_account_file(
|
||||
'service-account.json',
|
||||
scopes=['email'],
|
||||
subject='user@example.com'))
|
||||
|
||||
|
||||
The credentials are considered immutable. If you want to modify the scopes
|
||||
or the subject used for delegation, use :meth:`with_scopes` or
|
||||
:meth:`with_subject`::
|
||||
|
||||
scoped_credentials = credentials.with_scopes(['email'])
|
||||
delegated_credentials = credentials.with_subject(subject)
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
signer,
|
||||
service_account_email,
|
||||
token_uri,
|
||||
target_audience,
|
||||
additional_claims=None,
|
||||
quota_project_id=None,
|
||||
universe_domain=credentials.DEFAULT_UNIVERSE_DOMAIN,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
signer (google.auth.crypt.Signer): The signer used to sign JWTs.
|
||||
service_account_email (str): The service account's email.
|
||||
token_uri (str): The OAuth 2.0 Token URI.
|
||||
target_audience (str): The intended audience for these credentials,
|
||||
used when requesting the ID Token. The ID Token's ``aud`` claim
|
||||
will be set to this string.
|
||||
additional_claims (Mapping[str, str]): Any additional claims for
|
||||
the JWT assertion used in the authorization grant.
|
||||
quota_project_id (Optional[str]): The project ID used for quota and billing.
|
||||
universe_domain (str): The universe domain. The default
|
||||
universe domain is googleapis.com. For default value IAM ID
|
||||
token endponint is used for token refresh. Note that
|
||||
iam.serviceAccountTokenCreator role is required to use the IAM
|
||||
endpoint.
|
||||
.. note:: Typically one of the helper constructors
|
||||
:meth:`from_service_account_file` or
|
||||
:meth:`from_service_account_info` are used instead of calling the
|
||||
constructor directly.
|
||||
"""
|
||||
super(IDTokenCredentials, self).__init__()
|
||||
self._signer = signer
|
||||
self._service_account_email = service_account_email
|
||||
self._token_uri = token_uri
|
||||
self._target_audience = target_audience
|
||||
self._quota_project_id = quota_project_id
|
||||
self._use_iam_endpoint = False
|
||||
|
||||
if not universe_domain:
|
||||
self._universe_domain = credentials.DEFAULT_UNIVERSE_DOMAIN
|
||||
else:
|
||||
self._universe_domain = universe_domain
|
||||
self._iam_id_token_endpoint = iam._IAM_IDTOKEN_ENDPOINT.replace(
|
||||
"googleapis.com", self._universe_domain
|
||||
)
|
||||
|
||||
if self._universe_domain != credentials.DEFAULT_UNIVERSE_DOMAIN:
|
||||
self._use_iam_endpoint = True
|
||||
|
||||
if additional_claims is not None:
|
||||
self._additional_claims = additional_claims
|
||||
else:
|
||||
self._additional_claims = {}
|
||||
|
||||
@classmethod
|
||||
def _from_signer_and_info(cls, signer, info, **kwargs):
|
||||
"""Creates a credentials instance from a signer and service account
|
||||
info.
|
||||
|
||||
Args:
|
||||
signer (google.auth.crypt.Signer): The signer used to sign JWTs.
|
||||
info (Mapping[str, str]): The service account info.
|
||||
kwargs: Additional arguments to pass to the constructor.
|
||||
|
||||
Returns:
|
||||
google.auth.jwt.IDTokenCredentials: The constructed credentials.
|
||||
|
||||
Raises:
|
||||
ValueError: If the info is not in the expected format.
|
||||
"""
|
||||
kwargs.setdefault("service_account_email", info["client_email"])
|
||||
kwargs.setdefault("token_uri", info["token_uri"])
|
||||
if "universe_domain" in info:
|
||||
kwargs["universe_domain"] = info["universe_domain"]
|
||||
return cls(signer, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def from_service_account_info(cls, info, **kwargs):
|
||||
"""Creates a credentials instance from parsed service account info.
|
||||
|
||||
Args:
|
||||
info (Mapping[str, str]): The service account info in Google
|
||||
format.
|
||||
kwargs: Additional arguments to pass to the constructor.
|
||||
|
||||
Returns:
|
||||
google.auth.service_account.IDTokenCredentials: The constructed
|
||||
credentials.
|
||||
|
||||
Raises:
|
||||
ValueError: If the info is not in the expected format.
|
||||
"""
|
||||
signer = _service_account_info.from_dict(
|
||||
info, require=["client_email", "token_uri"]
|
||||
)
|
||||
return cls._from_signer_and_info(signer, info, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def from_service_account_file(cls, filename, **kwargs):
|
||||
"""Creates a credentials instance from a service account json file.
|
||||
|
||||
Args:
|
||||
filename (str): The path to the service account json file.
|
||||
kwargs: Additional arguments to pass to the constructor.
|
||||
|
||||
Returns:
|
||||
google.auth.service_account.IDTokenCredentials: The constructed
|
||||
credentials.
|
||||
"""
|
||||
info, signer = _service_account_info.from_filename(
|
||||
filename, require=["client_email", "token_uri"]
|
||||
)
|
||||
return cls._from_signer_and_info(signer, info, **kwargs)
|
||||
|
||||
def _make_copy(self):
|
||||
cred = self.__class__(
|
||||
self._signer,
|
||||
service_account_email=self._service_account_email,
|
||||
token_uri=self._token_uri,
|
||||
target_audience=self._target_audience,
|
||||
additional_claims=self._additional_claims.copy(),
|
||||
quota_project_id=self.quota_project_id,
|
||||
universe_domain=self._universe_domain,
|
||||
)
|
||||
# _use_iam_endpoint is not exposed in the constructor
|
||||
cred._use_iam_endpoint = self._use_iam_endpoint
|
||||
return cred
|
||||
|
||||
def with_target_audience(self, target_audience):
|
||||
"""Create a copy of these credentials with the specified target
|
||||
audience.
|
||||
|
||||
Args:
|
||||
target_audience (str): The intended audience for these credentials,
|
||||
used when requesting the ID Token.
|
||||
|
||||
Returns:
|
||||
google.auth.service_account.IDTokenCredentials: A new credentials
|
||||
instance.
|
||||
"""
|
||||
cred = self._make_copy()
|
||||
cred._target_audience = target_audience
|
||||
return cred
|
||||
|
||||
def _with_use_iam_endpoint(self, use_iam_endpoint):
|
||||
"""Create a copy of these credentials with the use_iam_endpoint value.
|
||||
|
||||
Args:
|
||||
use_iam_endpoint (bool): If True, IAM generateIdToken endpoint will
|
||||
be used instead of the token_uri. Note that
|
||||
iam.serviceAccountTokenCreator role is required to use the IAM
|
||||
endpoint. The default value is False. This feature is currently
|
||||
experimental and subject to change without notice.
|
||||
|
||||
Returns:
|
||||
google.auth.service_account.IDTokenCredentials: A new credentials
|
||||
instance.
|
||||
Raises:
|
||||
google.auth.exceptions.InvalidValue: If the universe domain is not
|
||||
default and use_iam_endpoint is False.
|
||||
"""
|
||||
cred = self._make_copy()
|
||||
if (
|
||||
cred._universe_domain != credentials.DEFAULT_UNIVERSE_DOMAIN
|
||||
and not use_iam_endpoint
|
||||
):
|
||||
raise exceptions.InvalidValue(
|
||||
"use_iam_endpoint should be True for non-default universe domain"
|
||||
)
|
||||
cred._use_iam_endpoint = use_iam_endpoint
|
||||
return cred
|
||||
|
||||
@_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
|
||||
def with_quota_project(self, quota_project_id):
|
||||
cred = self._make_copy()
|
||||
cred._quota_project_id = quota_project_id
|
||||
return cred
|
||||
|
||||
@_helpers.copy_docstring(credentials.CredentialsWithTokenUri)
|
||||
def with_token_uri(self, token_uri):
|
||||
cred = self._make_copy()
|
||||
cred._token_uri = token_uri
|
||||
return cred
|
||||
|
||||
def _make_authorization_grant_assertion(self):
|
||||
"""Create the OAuth 2.0 assertion.
|
||||
|
||||
This assertion is used during the OAuth 2.0 grant to acquire an
|
||||
ID token.
|
||||
|
||||
Returns:
|
||||
bytes: The authorization grant assertion.
|
||||
"""
|
||||
now = _helpers.utcnow()
|
||||
lifetime = datetime.timedelta(seconds=_DEFAULT_TOKEN_LIFETIME_SECS)
|
||||
expiry = now + lifetime
|
||||
|
||||
payload = {
|
||||
"iat": _helpers.datetime_to_secs(now),
|
||||
"exp": _helpers.datetime_to_secs(expiry),
|
||||
# The issuer must be the service account email.
|
||||
"iss": self.service_account_email,
|
||||
# The audience must be the auth token endpoint's URI
|
||||
"aud": _GOOGLE_OAUTH2_TOKEN_ENDPOINT,
|
||||
# The target audience specifies which service the ID token is
|
||||
# intended for.
|
||||
"target_audience": self._target_audience,
|
||||
}
|
||||
|
||||
payload.update(self._additional_claims)
|
||||
|
||||
token = jwt.encode(self._signer, payload)
|
||||
|
||||
return token
|
||||
|
||||
def _refresh_with_iam_endpoint(self, request):
|
||||
"""Use IAM generateIdToken endpoint to obtain an ID token.
|
||||
|
||||
It works as follows:
|
||||
|
||||
1. First we create a self signed jwt with
|
||||
https://www.googleapis.com/auth/iam being the scope.
|
||||
|
||||
2. Next we use the self signed jwt as the access token, and make a POST
|
||||
request to IAM generateIdToken endpoint. The request body is:
|
||||
{
|
||||
"audience": self._target_audience,
|
||||
"includeEmail": "true",
|
||||
"useEmailAzp": "true",
|
||||
}
|
||||
|
||||
If the request is succesfully, it will return {"token":"the ID token"},
|
||||
and we can extract the ID token and compute its expiry.
|
||||
"""
|
||||
jwt_credentials = jwt.Credentials.from_signing_credentials(
|
||||
self,
|
||||
None,
|
||||
additional_claims={"scope": "https://www.googleapis.com/auth/iam"},
|
||||
)
|
||||
jwt_credentials.refresh(request)
|
||||
self.token, self.expiry = _client.call_iam_generate_id_token_endpoint(
|
||||
request,
|
||||
self._iam_id_token_endpoint,
|
||||
self.signer_email,
|
||||
self._target_audience,
|
||||
jwt_credentials.token.decode(),
|
||||
self._universe_domain,
|
||||
)
|
||||
|
||||
@_helpers.copy_docstring(credentials.Credentials)
|
||||
def refresh(self, request):
|
||||
if self._use_iam_endpoint:
|
||||
self._refresh_with_iam_endpoint(request)
|
||||
else:
|
||||
assertion = self._make_authorization_grant_assertion()
|
||||
access_token, expiry, _ = _client.id_token_jwt_grant(
|
||||
request, self._token_uri, assertion
|
||||
)
|
||||
self.token = access_token
|
||||
self.expiry = expiry
|
||||
|
||||
@property
|
||||
def service_account_email(self):
|
||||
"""The service account email."""
|
||||
return self._service_account_email
|
||||
|
||||
@_helpers.copy_docstring(credentials.Signing)
|
||||
def sign_bytes(self, message):
|
||||
return self._signer.sign(message)
|
||||
|
||||
@property # type: ignore
|
||||
@_helpers.copy_docstring(credentials.Signing)
|
||||
def signer(self):
|
||||
return self._signer
|
||||
|
||||
@property # type: ignore
|
||||
@_helpers.copy_docstring(credentials.Signing)
|
||||
def signer_email(self):
|
||||
return self._service_account_email
|
||||
176
.venv/lib/python3.10/site-packages/google/oauth2/sts.py
Normal file
176
.venv/lib/python3.10/site-packages/google/oauth2/sts.py
Normal file
@@ -0,0 +1,176 @@
|
||||
# Copyright 2020 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""OAuth 2.0 Token Exchange Spec.
|
||||
|
||||
This module defines a token exchange utility based on the `OAuth 2.0 Token
|
||||
Exchange`_ spec. This will be mainly used to exchange external credentials
|
||||
for GCP access tokens in workload identity pools to access Google APIs.
|
||||
|
||||
The implementation will support various types of client authentication as
|
||||
allowed in the spec.
|
||||
|
||||
A deviation on the spec will be for additional Google specific options that
|
||||
cannot be easily mapped to parameters defined in the RFC.
|
||||
|
||||
The returned dictionary response will be based on the `rfc8693 section 2.2.1`_
|
||||
spec JSON response.
|
||||
|
||||
.. _OAuth 2.0 Token Exchange: https://tools.ietf.org/html/rfc8693
|
||||
.. _rfc8693 section 2.2.1: https://tools.ietf.org/html/rfc8693#section-2.2.1
|
||||
"""
|
||||
|
||||
import http.client as http_client
|
||||
import json
|
||||
import urllib
|
||||
|
||||
from google.oauth2 import utils
|
||||
|
||||
|
||||
_URLENCODED_HEADERS = {"Content-Type": "application/x-www-form-urlencoded"}
|
||||
|
||||
|
||||
class Client(utils.OAuthClientAuthHandler):
|
||||
"""Implements the OAuth 2.0 token exchange spec based on
|
||||
https://tools.ietf.org/html/rfc8693.
|
||||
"""
|
||||
|
||||
def __init__(self, token_exchange_endpoint, client_authentication=None):
|
||||
"""Initializes an STS client instance.
|
||||
|
||||
Args:
|
||||
token_exchange_endpoint (str): The token exchange endpoint.
|
||||
client_authentication (Optional(google.oauth2.oauth2_utils.ClientAuthentication)):
|
||||
The optional OAuth client authentication credentials if available.
|
||||
"""
|
||||
super(Client, self).__init__(client_authentication)
|
||||
self._token_exchange_endpoint = token_exchange_endpoint
|
||||
|
||||
def _make_request(self, request, headers, request_body):
|
||||
# Initialize request headers.
|
||||
request_headers = _URLENCODED_HEADERS.copy()
|
||||
|
||||
# Inject additional headers.
|
||||
if headers:
|
||||
for k, v in dict(headers).items():
|
||||
request_headers[k] = v
|
||||
|
||||
# Apply OAuth client authentication.
|
||||
self.apply_client_authentication_options(request_headers, request_body)
|
||||
|
||||
# Execute request.
|
||||
response = request(
|
||||
url=self._token_exchange_endpoint,
|
||||
method="POST",
|
||||
headers=request_headers,
|
||||
body=urllib.parse.urlencode(request_body).encode("utf-8"),
|
||||
)
|
||||
|
||||
response_body = (
|
||||
response.data.decode("utf-8")
|
||||
if hasattr(response.data, "decode")
|
||||
else response.data
|
||||
)
|
||||
|
||||
# If non-200 response received, translate to OAuthError exception.
|
||||
if response.status != http_client.OK:
|
||||
utils.handle_error_response(response_body)
|
||||
|
||||
response_data = json.loads(response_body)
|
||||
|
||||
# Return successful response.
|
||||
return response_data
|
||||
|
||||
def exchange_token(
|
||||
self,
|
||||
request,
|
||||
grant_type,
|
||||
subject_token,
|
||||
subject_token_type,
|
||||
resource=None,
|
||||
audience=None,
|
||||
scopes=None,
|
||||
requested_token_type=None,
|
||||
actor_token=None,
|
||||
actor_token_type=None,
|
||||
additional_options=None,
|
||||
additional_headers=None,
|
||||
):
|
||||
"""Exchanges the provided token for another type of token based on the
|
||||
rfc8693 spec.
|
||||
|
||||
Args:
|
||||
request (google.auth.transport.Request): A callable used to make
|
||||
HTTP requests.
|
||||
grant_type (str): The OAuth 2.0 token exchange grant type.
|
||||
subject_token (str): The OAuth 2.0 token exchange subject token.
|
||||
subject_token_type (str): The OAuth 2.0 token exchange subject token type.
|
||||
resource (Optional[str]): The optional OAuth 2.0 token exchange resource field.
|
||||
audience (Optional[str]): The optional OAuth 2.0 token exchange audience field.
|
||||
scopes (Optional[Sequence[str]]): The optional list of scopes to use.
|
||||
requested_token_type (Optional[str]): The optional OAuth 2.0 token exchange requested
|
||||
token type.
|
||||
actor_token (Optional[str]): The optional OAuth 2.0 token exchange actor token.
|
||||
actor_token_type (Optional[str]): The optional OAuth 2.0 token exchange actor token type.
|
||||
additional_options (Optional[Mapping[str, str]]): The optional additional
|
||||
non-standard Google specific options.
|
||||
additional_headers (Optional[Mapping[str, str]]): The optional additional
|
||||
headers to pass to the token exchange endpoint.
|
||||
|
||||
Returns:
|
||||
Mapping[str, str]: The token exchange JSON-decoded response data containing
|
||||
the requested token and its expiration time.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.OAuthError: If the token endpoint returned
|
||||
an error.
|
||||
"""
|
||||
# Initialize request body.
|
||||
request_body = {
|
||||
"grant_type": grant_type,
|
||||
"resource": resource,
|
||||
"audience": audience,
|
||||
"scope": " ".join(scopes or []),
|
||||
"requested_token_type": requested_token_type,
|
||||
"subject_token": subject_token,
|
||||
"subject_token_type": subject_token_type,
|
||||
"actor_token": actor_token,
|
||||
"actor_token_type": actor_token_type,
|
||||
"options": None,
|
||||
}
|
||||
# Add additional non-standard options.
|
||||
if additional_options:
|
||||
request_body["options"] = urllib.parse.quote(json.dumps(additional_options))
|
||||
# Remove empty fields in request body.
|
||||
for k, v in dict(request_body).items():
|
||||
if v is None or v == "":
|
||||
del request_body[k]
|
||||
|
||||
return self._make_request(request, additional_headers, request_body)
|
||||
|
||||
def refresh_token(self, request, refresh_token):
|
||||
"""Exchanges a refresh token for an access token based on the
|
||||
RFC6749 spec.
|
||||
|
||||
Args:
|
||||
request (google.auth.transport.Request): A callable used to make
|
||||
HTTP requests.
|
||||
subject_token (str): The OAuth 2.0 refresh token.
|
||||
"""
|
||||
|
||||
return self._make_request(
|
||||
request,
|
||||
None,
|
||||
{"grant_type": "refresh_token", "refresh_token": refresh_token},
|
||||
)
|
||||
168
.venv/lib/python3.10/site-packages/google/oauth2/utils.py
Normal file
168
.venv/lib/python3.10/site-packages/google/oauth2/utils.py
Normal file
@@ -0,0 +1,168 @@
|
||||
# Copyright 2020 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""OAuth 2.0 Utilities.
|
||||
|
||||
This module provides implementations for various OAuth 2.0 utilities.
|
||||
This includes `OAuth error handling`_ and
|
||||
`Client authentication for OAuth flows`_.
|
||||
|
||||
OAuth error handling
|
||||
--------------------
|
||||
This will define interfaces for handling OAuth related error responses as
|
||||
stated in `RFC 6749 section 5.2`_.
|
||||
This will include a common function to convert these HTTP error responses to a
|
||||
:class:`google.auth.exceptions.OAuthError` exception.
|
||||
|
||||
|
||||
Client authentication for OAuth flows
|
||||
-------------------------------------
|
||||
We introduce an interface for defining client authentication credentials based
|
||||
on `RFC 6749 section 2.3.1`_. This will expose the following
|
||||
capabilities:
|
||||
|
||||
* Ability to support basic authentication via request header.
|
||||
* Ability to support bearer token authentication via request header.
|
||||
* Ability to support client ID / secret authentication via request body.
|
||||
|
||||
.. _RFC 6749 section 2.3.1: https://tools.ietf.org/html/rfc6749#section-2.3.1
|
||||
.. _RFC 6749 section 5.2: https://tools.ietf.org/html/rfc6749#section-5.2
|
||||
"""
|
||||
|
||||
import abc
|
||||
import base64
|
||||
import enum
|
||||
import json
|
||||
|
||||
from google.auth import exceptions
|
||||
|
||||
|
||||
# OAuth client authentication based on
|
||||
# https://tools.ietf.org/html/rfc6749#section-2.3.
|
||||
class ClientAuthType(enum.Enum):
|
||||
basic = 1
|
||||
request_body = 2
|
||||
|
||||
|
||||
class ClientAuthentication(object):
|
||||
"""Defines the client authentication credentials for basic and request-body
|
||||
types based on https://tools.ietf.org/html/rfc6749#section-2.3.1.
|
||||
"""
|
||||
|
||||
def __init__(self, client_auth_type, client_id, client_secret=None):
|
||||
"""Instantiates a client authentication object containing the client ID
|
||||
and secret credentials for basic and response-body auth.
|
||||
|
||||
Args:
|
||||
client_auth_type (google.oauth2.oauth_utils.ClientAuthType): The
|
||||
client authentication type.
|
||||
client_id (str): The client ID.
|
||||
client_secret (Optional[str]): The client secret.
|
||||
"""
|
||||
self.client_auth_type = client_auth_type
|
||||
self.client_id = client_id
|
||||
self.client_secret = client_secret
|
||||
|
||||
|
||||
class OAuthClientAuthHandler(metaclass=abc.ABCMeta):
|
||||
"""Abstract class for handling client authentication in OAuth-based
|
||||
operations.
|
||||
"""
|
||||
|
||||
def __init__(self, client_authentication=None):
|
||||
"""Instantiates an OAuth client authentication handler.
|
||||
|
||||
Args:
|
||||
client_authentication (Optional[google.oauth2.utils.ClientAuthentication]):
|
||||
The OAuth client authentication credentials if available.
|
||||
"""
|
||||
super(OAuthClientAuthHandler, self).__init__()
|
||||
self._client_authentication = client_authentication
|
||||
|
||||
def apply_client_authentication_options(
|
||||
self, headers, request_body=None, bearer_token=None
|
||||
):
|
||||
"""Applies client authentication on the OAuth request's headers or POST
|
||||
body.
|
||||
|
||||
Args:
|
||||
headers (Mapping[str, str]): The HTTP request header.
|
||||
request_body (Optional[Mapping[str, str]]): The HTTP request body
|
||||
dictionary. For requests that do not support request body, this
|
||||
is None and will be ignored.
|
||||
bearer_token (Optional[str]): The optional bearer token.
|
||||
"""
|
||||
# Inject authenticated header.
|
||||
self._inject_authenticated_headers(headers, bearer_token)
|
||||
# Inject authenticated request body.
|
||||
if bearer_token is None:
|
||||
self._inject_authenticated_request_body(request_body)
|
||||
|
||||
def _inject_authenticated_headers(self, headers, bearer_token=None):
|
||||
if bearer_token is not None:
|
||||
headers["Authorization"] = "Bearer %s" % bearer_token
|
||||
elif (
|
||||
self._client_authentication is not None
|
||||
and self._client_authentication.client_auth_type is ClientAuthType.basic
|
||||
):
|
||||
username = self._client_authentication.client_id
|
||||
password = self._client_authentication.client_secret or ""
|
||||
|
||||
credentials = base64.b64encode(
|
||||
("%s:%s" % (username, password)).encode()
|
||||
).decode()
|
||||
headers["Authorization"] = "Basic %s" % credentials
|
||||
|
||||
def _inject_authenticated_request_body(self, request_body):
|
||||
if (
|
||||
self._client_authentication is not None
|
||||
and self._client_authentication.client_auth_type
|
||||
is ClientAuthType.request_body
|
||||
):
|
||||
if request_body is None:
|
||||
raise exceptions.OAuthError(
|
||||
"HTTP request does not support request-body"
|
||||
)
|
||||
else:
|
||||
request_body["client_id"] = self._client_authentication.client_id
|
||||
request_body["client_secret"] = (
|
||||
self._client_authentication.client_secret or ""
|
||||
)
|
||||
|
||||
|
||||
def handle_error_response(response_body):
|
||||
"""Translates an error response from an OAuth operation into an
|
||||
OAuthError exception.
|
||||
|
||||
Args:
|
||||
response_body (str): The decoded response data.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.OAuthError
|
||||
"""
|
||||
try:
|
||||
error_components = []
|
||||
error_data = json.loads(response_body)
|
||||
|
||||
error_components.append("Error code {}".format(error_data["error"]))
|
||||
if "error_description" in error_data:
|
||||
error_components.append(": {}".format(error_data["error_description"]))
|
||||
if "error_uri" in error_data:
|
||||
error_components.append(" - {}".format(error_data["error_uri"]))
|
||||
error_details = "".join(error_components)
|
||||
# If no details could be extracted, use the response data.
|
||||
except (KeyError, ValueError):
|
||||
error_details = response_body
|
||||
|
||||
raise exceptions.OAuthError(error_details, response_body)
|
||||
@@ -0,0 +1,82 @@
|
||||
import abc
|
||||
import os
|
||||
import struct
|
||||
import subprocess
|
||||
|
||||
from google.auth import exceptions
|
||||
from google.oauth2.webauthn_types import GetRequest, GetResponse
|
||||
|
||||
|
||||
class WebAuthnHandler(abc.ABC):
|
||||
@abc.abstractmethod
|
||||
def is_available(self) -> bool:
|
||||
"""Check whether this WebAuthn handler is available"""
|
||||
raise NotImplementedError("is_available method must be implemented")
|
||||
|
||||
@abc.abstractmethod
|
||||
def get(self, get_request: GetRequest) -> GetResponse:
|
||||
"""WebAuthn get (assertion)"""
|
||||
raise NotImplementedError("get method must be implemented")
|
||||
|
||||
|
||||
class PluginHandler(WebAuthnHandler):
|
||||
"""Offloads WebAuthn get reqeust to a pluggable command-line tool.
|
||||
|
||||
Offloads WebAuthn get to a plugin which takes the form of a
|
||||
command-line tool. The command-line tool is configurable via the
|
||||
PluginHandler._ENV_VAR environment variable.
|
||||
|
||||
The WebAuthn plugin should implement the following interface:
|
||||
|
||||
Communication occurs over stdin/stdout, and messages are both sent and
|
||||
received in the form:
|
||||
|
||||
[4 bytes - payload size (little-endian)][variable bytes - json payload]
|
||||
"""
|
||||
|
||||
_ENV_VAR = "GOOGLE_AUTH_WEBAUTHN_PLUGIN"
|
||||
|
||||
def is_available(self) -> bool:
|
||||
try:
|
||||
self._find_plugin()
|
||||
except Exception:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def get(self, get_request: GetRequest) -> GetResponse:
|
||||
request_json = get_request.to_json()
|
||||
cmd = self._find_plugin()
|
||||
response_json = self._call_plugin(cmd, request_json)
|
||||
return GetResponse.from_json(response_json)
|
||||
|
||||
def _call_plugin(self, cmd: str, input_json: str) -> str:
|
||||
# Calculate length of input
|
||||
input_length = len(input_json)
|
||||
length_bytes_le = struct.pack("<I", input_length)
|
||||
request = length_bytes_le + input_json.encode()
|
||||
|
||||
# Call plugin
|
||||
process_result = subprocess.run(
|
||||
[cmd], input=request, capture_output=True, check=True
|
||||
)
|
||||
|
||||
# Check length of response
|
||||
response_len_le = process_result.stdout[:4]
|
||||
response_len = struct.unpack("<I", response_len_le)[0]
|
||||
response = process_result.stdout[4:]
|
||||
if response_len != len(response):
|
||||
raise exceptions.MalformedError(
|
||||
"Plugin response length {} does not match data {}".format(
|
||||
response_len, len(response)
|
||||
)
|
||||
)
|
||||
return response.decode()
|
||||
|
||||
def _find_plugin(self) -> str:
|
||||
plugin_cmd = os.environ.get(PluginHandler._ENV_VAR)
|
||||
if plugin_cmd is None:
|
||||
raise exceptions.InvalidResource(
|
||||
"{} env var is not set".format(PluginHandler._ENV_VAR)
|
||||
)
|
||||
return plugin_cmd
|
||||
@@ -0,0 +1,16 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from google.oauth2.webauthn_handler import PluginHandler, WebAuthnHandler
|
||||
|
||||
|
||||
class WebauthnHandlerFactory:
|
||||
handlers: List[WebAuthnHandler]
|
||||
|
||||
def __init__(self):
|
||||
self.handlers = [PluginHandler()]
|
||||
|
||||
def get_handler(self) -> Optional[WebAuthnHandler]:
|
||||
for handler in self.handlers:
|
||||
if handler.is_available():
|
||||
return handler
|
||||
return None
|
||||
@@ -0,0 +1,156 @@
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from google.auth import exceptions
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PublicKeyCredentialDescriptor:
|
||||
"""Descriptor for a security key based credential.
|
||||
|
||||
https://www.w3.org/TR/webauthn-3/#dictionary-credential-descriptor
|
||||
|
||||
Args:
|
||||
id: <url-safe base64-encoded> credential id (key handle).
|
||||
transports: <'usb'|'nfc'|'ble'|'internal'> List of supported transports.
|
||||
"""
|
||||
|
||||
id: str
|
||||
transports: Optional[List[str]] = None
|
||||
|
||||
def to_dict(self):
|
||||
cred = {"type": "public-key", "id": self.id}
|
||||
if self.transports:
|
||||
cred["transports"] = self.transports
|
||||
return cred
|
||||
|
||||
|
||||
@dataclass
|
||||
class AuthenticationExtensionsClientInputs:
|
||||
"""Client extensions inputs for WebAuthn extensions.
|
||||
|
||||
Args:
|
||||
appid: app id that can be asserted with in addition to rpid.
|
||||
https://www.w3.org/TR/webauthn-3/#sctn-appid-extension
|
||||
"""
|
||||
|
||||
appid: Optional[str] = None
|
||||
|
||||
def to_dict(self):
|
||||
extensions = {}
|
||||
if self.appid:
|
||||
extensions["appid"] = self.appid
|
||||
return extensions
|
||||
|
||||
|
||||
@dataclass
|
||||
class GetRequest:
|
||||
"""WebAuthn get request
|
||||
|
||||
Args:
|
||||
origin: Origin where the WebAuthn get assertion takes place.
|
||||
rpid: Relying Party ID.
|
||||
challenge: <url-safe base64-encoded> raw challenge.
|
||||
timeout_ms: Timeout number in millisecond.
|
||||
allow_credentials: List of allowed credentials.
|
||||
user_verification: <'required'|'preferred'|'discouraged'> User verification requirement.
|
||||
extensions: WebAuthn authentication extensions inputs.
|
||||
"""
|
||||
|
||||
origin: str
|
||||
rpid: str
|
||||
challenge: str
|
||||
timeout_ms: Optional[int] = None
|
||||
allow_credentials: Optional[List[PublicKeyCredentialDescriptor]] = None
|
||||
user_verification: Optional[str] = None
|
||||
extensions: Optional[AuthenticationExtensionsClientInputs] = None
|
||||
|
||||
def to_json(self) -> str:
|
||||
req_options: Dict[str, Any] = {"rpid": self.rpid, "challenge": self.challenge}
|
||||
if self.timeout_ms:
|
||||
req_options["timeout"] = self.timeout_ms
|
||||
if self.allow_credentials:
|
||||
req_options["allowCredentials"] = [
|
||||
c.to_dict() for c in self.allow_credentials
|
||||
]
|
||||
if self.user_verification:
|
||||
req_options["userVerification"] = self.user_verification
|
||||
if self.extensions:
|
||||
req_options["extensions"] = self.extensions.to_dict()
|
||||
return json.dumps(
|
||||
{"type": "get", "origin": self.origin, "requestData": req_options}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AuthenticatorAssertionResponse:
|
||||
"""Authenticator response to a WebAuthn get (assertion) request.
|
||||
|
||||
https://www.w3.org/TR/webauthn-3/#authenticatorassertionresponse
|
||||
|
||||
Args:
|
||||
client_data_json: <url-safe base64-encoded> client data JSON.
|
||||
authenticator_data: <url-safe base64-encoded> authenticator data.
|
||||
signature: <url-safe base64-encoded> signature.
|
||||
user_handle: <url-safe base64-encoded> user handle.
|
||||
"""
|
||||
|
||||
client_data_json: str
|
||||
authenticator_data: str
|
||||
signature: str
|
||||
user_handle: Optional[str]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GetResponse:
|
||||
"""WebAuthn get (assertion) response.
|
||||
|
||||
Args:
|
||||
id: <url-safe base64-encoded> credential id (key handle).
|
||||
response: The authenticator assertion response.
|
||||
authenticator_attachment: <'cross-platform'|'platform'> The attachment status of the authenticator.
|
||||
client_extension_results: WebAuthn authentication extensions output results in a dictionary.
|
||||
"""
|
||||
|
||||
id: str
|
||||
response: AuthenticatorAssertionResponse
|
||||
authenticator_attachment: Optional[str]
|
||||
client_extension_results: Optional[Dict]
|
||||
|
||||
@staticmethod
|
||||
def from_json(json_str: str):
|
||||
"""Verify and construct GetResponse from a JSON string."""
|
||||
try:
|
||||
resp_json = json.loads(json_str)
|
||||
except ValueError:
|
||||
raise exceptions.MalformedError("Invalid Get JSON response")
|
||||
if resp_json.get("type") != "getResponse":
|
||||
raise exceptions.MalformedError(
|
||||
"Invalid Get response type: {}".format(resp_json.get("type"))
|
||||
)
|
||||
pk_cred = resp_json.get("responseData")
|
||||
if pk_cred is None:
|
||||
if resp_json.get("error"):
|
||||
raise exceptions.ReauthFailError(
|
||||
"WebAuthn.get failure: {}".format(resp_json["error"])
|
||||
)
|
||||
else:
|
||||
raise exceptions.MalformedError("Get response is empty")
|
||||
if pk_cred.get("type") != "public-key":
|
||||
raise exceptions.MalformedError(
|
||||
"Invalid credential type: {}".format(pk_cred.get("type"))
|
||||
)
|
||||
assertion_json = pk_cred["response"]
|
||||
assertion_resp = AuthenticatorAssertionResponse(
|
||||
client_data_json=assertion_json["clientDataJSON"],
|
||||
authenticator_data=assertion_json["authenticatorData"],
|
||||
signature=assertion_json["signature"],
|
||||
user_handle=assertion_json.get("userHandle"),
|
||||
)
|
||||
return GetResponse(
|
||||
id=pk_cred["id"],
|
||||
response=assertion_resp,
|
||||
authenticator_attachment=pk_cred.get("authenticatorAttachment"),
|
||||
client_extension_results=pk_cred.get("clientExtensionResults"),
|
||||
)
|
||||
Reference in New Issue
Block a user