Added delete option to database storage.

This commit is contained in:
Batuhan Berk Başoğlu 2020-10-12 12:10:01 -04:00
parent 308604a33c
commit 963b5bc68b
1868 changed files with 192402 additions and 13278 deletions

View file

@ -0,0 +1,15 @@
# 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."""

View file

@ -0,0 +1,259 @@
# 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 json
import six
from six.moves import http_client
from six.moves import urllib
from google.auth import _helpers
from google.auth import exceptions
from google.auth import jwt
_URLENCODED_CONTENT_TYPE = "application/x-www-form-urlencoded"
_JWT_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer"
_REFRESH_GRANT_TYPE = "refresh_token"
def _handle_error_response(response_body):
""""Translates an error response into an exception.
Args:
response_body (str): The decoded response data.
Raises:
google.auth.exceptions.RefreshError
"""
try:
error_data = json.loads(response_body)
error_details = "{}: {}".format(
error_data["error"], error_data.get("error_description")
)
# If no details could be extracted, use the response data.
except (KeyError, ValueError):
error_details = response_body
raise exceptions.RefreshError(error_details, response_body)
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:
return _helpers.utcnow() + datetime.timedelta(seconds=expires_in)
else:
return None
def _token_endpoint_request(request, token_uri, body):
"""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.
Returns:
Mapping[str, str]: The JSON-decoded response data.
Raises:
google.auth.exceptions.RefreshError: If the token endpoint returned
an error.
"""
body = urllib.parse.urlencode(body).encode("utf-8")
headers = {"content-type": _URLENCODED_CONTENT_TYPE}
retry = 0
# retry to fetch token for maximum of two times if any internal failure
# occurs.
while True:
response = request(method="POST", url=token_uri, headers=headers, body=body)
response_body = (
response.data.decode("utf-8")
if hasattr(response.data, "decode")
else response.data
)
response_data = json.loads(response_body)
if response.status == http_client.OK:
break
else:
error_desc = response_data.get("error_description") or ""
error_code = response_data.get("error") or ""
if (
any(e == "internal_failure" for e in (error_code, error_desc))
and retry < 1
):
retry += 1
continue
_handle_error_response(response_body)
return response_data
def jwt_grant(request, token_uri, assertion):
"""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.
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)
try:
access_token = response_data["access_token"]
except KeyError as caught_exc:
new_exc = exceptions.RefreshError("No access token in response.", response_data)
six.raise_from(new_exc, caught_exc)
expiry = _parse_expiry(response_data)
return access_token, expiry, response_data
def id_token_jwt_grant(request, token_uri, assertion):
"""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.
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)
try:
id_token = response_data["id_token"]
except KeyError as caught_exc:
new_exc = exceptions.RefreshError("No ID token in response.", response_data)
six.raise_from(new_exc, caught_exc)
payload = jwt.decode(id_token, verify=False)
expiry = datetime.datetime.utcfromtimestamp(payload["exp"])
return id_token, expiry, response_data
def refresh_grant(
request, token_uri, refresh_token, client_id, client_secret, scopes=None
):
"""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').
Returns:
Tuple[str, Optional[str], Optional[datetime], Mapping[str, str]]: The
access token, new 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)
response_data = _token_endpoint_request(request, token_uri, body)
try:
access_token = response_data["access_token"]
except KeyError as caught_exc:
new_exc = exceptions.RefreshError("No access token in response.", response_data)
six.raise_from(new_exc, caught_exc)
refresh_token = response_data.get("refresh_token", refresh_token)
expiry = _parse_expiry(response_data)
return access_token, refresh_token, expiry, response_data

View file

@ -0,0 +1,264 @@
# 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 json
import six
from six.moves import http_client
from six.moves import urllib
from google.auth import _helpers
from google.auth import exceptions
from google.auth import jwt
from google.oauth2 import _client as client
def _handle_error_response(response_body):
""""Translates an error response into an exception.
Args:
response_body (str): The decoded response data.
Raises:
google.auth.exceptions.RefreshError
"""
try:
error_data = json.loads(response_body)
error_details = "{}: {}".format(
error_data["error"], error_data.get("error_description")
)
# If no details could be extracted, use the response data.
except (KeyError, ValueError):
error_details = response_body
raise exceptions.RefreshError(error_details, response_body)
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:
return _helpers.utcnow() + datetime.timedelta(seconds=expires_in)
else:
return None
async def _token_endpoint_request(request, token_uri, body):
"""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.
Returns:
Mapping[str, str]: The JSON-decoded response data.
Raises:
google.auth.exceptions.RefreshError: If the token endpoint returned
an error.
"""
body = urllib.parse.urlencode(body).encode("utf-8")
headers = {"content-type": client._URLENCODED_CONTENT_TYPE}
retry = 0
# retry to fetch token for maximum of two times if any internal failure
# occurs.
while True:
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
)
response_data = json.loads(response_body)
if response.status == http_client.OK:
break
else:
error_desc = response_data.get("error_description") or ""
error_code = response_data.get("error") or ""
if (
any(e == "internal_failure" for e in (error_code, error_desc))
and retry < 1
):
retry += 1
continue
_handle_error_response(response_body)
return response_data
async def jwt_grant(request, token_uri, assertion):
"""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.
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)
try:
access_token = response_data["access_token"]
except KeyError as caught_exc:
new_exc = exceptions.RefreshError("No access token in response.", response_data)
six.raise_from(new_exc, caught_exc)
expiry = _parse_expiry(response_data)
return access_token, expiry, response_data
async def id_token_jwt_grant(request, token_uri, assertion):
"""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.
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)
try:
id_token = response_data["id_token"]
except KeyError as caught_exc:
new_exc = exceptions.RefreshError("No ID token in response.", response_data)
six.raise_from(new_exc, 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
):
"""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').
Returns:
Tuple[str, Optional[str], Optional[datetime], Mapping[str, str]]: The
access token, new 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)
response_data = await _token_endpoint_request(request, token_uri, body)
try:
access_token = response_data["access_token"]
except KeyError as caught_exc:
new_exc = exceptions.RefreshError("No access token in response.", response_data)
six.raise_from(new_exc, caught_exc)
refresh_token = response_data.get("refresh_token", refresh_token)
expiry = _parse_expiry(response_data)
return access_token, refresh_token, expiry, response_data

View file

@ -0,0 +1,108 @@
# 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 _client_async as _client
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,
) = await _client.refresh_grant(
request,
self._token_uri,
self._refresh_token,
self._client_id,
self._client_secret,
self._scopes,
)
self.token = access_token
self.expiry = expiry
self._refresh_token = refresh_token
self._id_token = grant_response.get("id_token")
if self._scopes and "scopes" in grant_response:
requested_scopes = frozenset(self._scopes)
granted_scopes = frozenset(grant_response["scopes"].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)
)
)
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.
"""

View file

@ -0,0 +1,267 @@
# 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 json
import os
import six
from six.moves import http_client
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.data.read()
return json.loads(json.dumps(data))
async def verify_token(
id_token, request, audience=None, certs_url=sync_id_token._GOOGLE_OAUTH2_CERTS_URL
):
"""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'}``.
Returns:
Mapping[str, Any]: The decoded token.
"""
certs = await _fetch_certs(request, certs_url)
return jwt.decode(id_token, certs=certs, audience=audience)
async def verify_oauth2_token(id_token, request, audience=None):
"""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.
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,
)
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):
"""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.
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,
)
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:
1. If the application is running in Compute Engine, App Engine or Cloud Run,
then the ID token are obtained from the metadata server.
2. 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.
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. First try to fetch ID token from metadata server if it exists. The code
# works for GAE and Cloud Run metadata server as well.
try:
from google.auth import compute_engine
request_new = requests.Request()
credentials = compute_engine.IDTokenCredentials(
request_new, audience, use_metadata_identity_endpoint=True
)
credentials.refresh(request_new)
return credentials.token
except (ImportError, exceptions.TransportError, exceptions.RefreshError):
pass
# 2. Try to use service account credentials to get ID token.
# Try to get credentials from the GOOGLE_APPLICATION_CREDENTIALS environment
# variable.
credentials_filename = os.environ.get(environment_vars.CREDENTIALS)
if not (
credentials_filename
and os.path.exists(credentials_filename)
and os.path.isfile(credentials_filename)
):
raise exceptions.DefaultCredentialsError(
"Neither metadata server or valid service account credentials are found."
)
try:
with open(credentials_filename, "r") as f:
info = json.load(f)
credentials_content = (
(info.get("type") == "service_account") and info or None
)
from google.oauth2 import _service_account_async as service_account
credentials = service_account.IDTokenCredentials.from_service_account_info(
credentials_content, target_audience=audience
)
except ValueError as caught_exc:
new_exc = exceptions.DefaultCredentialsError(
"Neither metadata server or valid service account credentials are found.",
caught_exc,
)
six.raise_from(new_exc, caught_exc)
await credentials.refresh(request)
return credentials.token

View file

@ -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

View file

@ -0,0 +1,383 @@
# 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 six
from google.auth import _cloud_sdk
from google.auth import _helpers
from google.auth import credentials
from google.auth import exceptions
from google.oauth2 import _client
# The Google OAuth 2.0 token endpoint. Used for authorized user credentials.
_GOOGLE_OAUTH2_TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token"
class Credentials(credentials.ReadOnlyScoped, credentials.CredentialsWithQuotaProject):
"""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)
"""
def __init__(
self,
token,
refresh_token=None,
id_token=None,
token_uri=None,
client_id=None,
client_secret=None,
scopes=None,
quota_project_id=None,
expiry=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').
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.
"""
super(Credentials, self).__init__()
self.token = token
self.expiry = expiry
self._refresh_token = refresh_token
self._id_token = id_token
self._scopes = scopes
self._token_uri = token_uri
self._client_id = client_id
self._client_secret = client_secret
self._quota_project_id = quota_project_id
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__
"""
return self.__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._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")
@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 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
@_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
def with_quota_project(self, quota_project_id):
return 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,
quota_project_id=quota_project_id,
)
@_helpers.copy_docstring(credentials.Credentials)
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 = _client.refresh_grant(
request,
self._token_uri,
self._refresh_token,
self._client_id,
self._client_secret,
self._scopes,
)
self.token = access_token
self.expiry = expiry
self._refresh_token = refresh_token
self._id_token = grant_response.get("id_token")
if self._scopes and "scopes" in grant_response:
requested_scopes = frozenset(self._scopes)
granted_scopes = frozenset(grant_response["scopes"].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)
)
)
@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(six.iterkeys(info))
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.CLOCK_SKEW
# 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,
)
@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,
}
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):
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)

View file

@ -0,0 +1,266 @@
# 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')
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 json
import os
import six
from six.moves import http_client
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'}``.
Args:
request (google.auth.transport.Request): The object used to make
HTTP requests.
certs_url (str): The certificate endpoint URL.
Returns:
Mapping[str, str]: A mapping of public key ID to x.509 certificate
data.
"""
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):
"""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): 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'}``.
Returns:
Mapping[str, Any]: The decoded token.
"""
certs = _fetch_certs(request, certs_url)
return jwt.decode(id_token, certs=certs, audience=audience)
def verify_oauth2_token(id_token, request, audience=None):
"""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.
Returns:
Mapping[str, Any]: The decoded token.
Raises:
exceptions.GoogleAuthError: If the issuer is invalid.
"""
idinfo = verify_token(
id_token, request, audience=audience, certs_url=_GOOGLE_OAUTH2_CERTS_URL
)
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):
"""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.
Returns:
Mapping[str, Any]: The decoded token.
"""
return verify_token(
id_token, request, audience=audience, certs_url=_GOOGLE_APIS_CERTS_URL
)
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:
1. If the application is running in Compute Engine, App Engine or Cloud Run,
then the ID token are obtained from the metadata server.
2. 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.
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.
"""
# 1. First 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
credentials = compute_engine.IDTokenCredentials(
request, audience, use_metadata_identity_endpoint=True
)
credentials.refresh(request)
return credentials.token
except (ImportError, exceptions.TransportError, exceptions.RefreshError):
pass
# 2. Try to use service account credentials to get ID token.
# Try to get credentials from the GOOGLE_APPLICATION_CREDENTIALS environment
# variable.
credentials_filename = os.environ.get(environment_vars.CREDENTIALS)
if not (
credentials_filename
and os.path.exists(credentials_filename)
and os.path.isfile(credentials_filename)
):
raise exceptions.DefaultCredentialsError(
"Neither metadata server or valid service account credentials are found."
)
try:
with open(credentials_filename, "r") as f:
info = json.load(f)
credentials_content = (
(info.get("type") == "service_account") and info or None
)
from google.oauth2 import service_account
credentials = service_account.IDTokenCredentials.from_service_account_info(
credentials_content, target_audience=audience
)
except ValueError as caught_exc:
new_exc = exceptions.DefaultCredentialsError(
"Neither metadata server or valid service account credentials are found.",
caught_exc,
)
six.raise_from(new_exc, caught_exc)
credentials.refresh(request)
return credentials.token

View file

@ -0,0 +1,606 @@
# 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 jwt
from google.oauth2 import _client
_DEFAULT_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
class Credentials(
credentials.Signing, credentials.Scoped, credentials.CredentialsWithQuotaProject
):
"""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,
subject=None,
project_id=None,
quota_project_id=None,
additional_claims=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]): Scopes to request during the authorization
grant.
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.
.. 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._scopes = 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
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.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"),
**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
@_helpers.copy_docstring(credentials.Scoped)
def with_scopes(self, scopes):
return self.__class__(
self._signer,
service_account_email=self._service_account_email,
scopes=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(),
)
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.
"""
return self.__class__(
self._signer,
service_account_email=self._service_account_email,
scopes=self._scopes,
token_uri=self._token_uri,
subject=subject,
project_id=self._project_id,
quota_project_id=self._quota_project_id,
additional_claims=self._additional_claims.copy(),
)
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 {})
return self.__class__(
self._signer,
service_account_email=self._service_account_email,
scopes=self._scopes,
token_uri=self._token_uri,
subject=self._subject,
project_id=self._project_id,
quota_project_id=self._quota_project_id,
additional_claims=new_additional_claims,
)
@_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
def with_quota_project(self, quota_project_id):
return self.__class__(
self._signer,
service_account_email=self._service_account_email,
scopes=self._scopes,
token_uri=self._token_uri,
subject=self._subject,
project_id=self._project_id,
quota_project_id=quota_project_id,
additional_claims=self._additional_claims.copy(),
)
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": self._token_uri,
"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
@_helpers.copy_docstring(credentials.Credentials)
def refresh(self, request):
assertion = self._make_authorization_grant_assertion()
access_token, expiry, _ = _client.jwt_grant(request, self._token_uri, assertion)
self.token = access_token
self.expiry = expiry
@_helpers.copy_docstring(credentials.Signing)
def sign_bytes(self, message):
return self._signer.sign(message)
@property
@_helpers.copy_docstring(credentials.Signing)
def signer(self):
return self._signer
@property
@_helpers.copy_docstring(credentials.Signing)
def signer_email(self):
return self._service_account_email
class IDTokenCredentials(credentials.Signing, credentials.CredentialsWithQuotaProject):
"""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,
):
"""
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.
.. 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
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"])
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 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.
"""
return self.__class__(
self._signer,
service_account_email=self._service_account_email,
token_uri=self._token_uri,
target_audience=target_audience,
additional_claims=self._additional_claims.copy(),
quota_project_id=self.quota_project_id,
)
@_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
def with_quota_project(self, quota_project_id):
return 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=quota_project_id,
)
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": self._token_uri,
# 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
@_helpers.copy_docstring(credentials.Credentials)
def refresh(self, request):
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
@_helpers.copy_docstring(credentials.Signing)
def signer(self):
return self._signer
@property
@_helpers.copy_docstring(credentials.Signing)
def signer_email(self):
return self._service_account_email