File migrate-to-google-auth.patch of Package python-pydrive2
From 2789fda4c4ee7aac002f561819f2cec68e838239 Mon Sep 17 00:00:00 2001
From: junpeng-jp <junpeng.ong@gmail.com>
Date: Sat, 6 Aug 2022 21:25:26 +0800
Subject: [PATCH 01/11] Migrate to Google Auth
---
pydrive2/auth.py | 606 +++++++++---------
pydrive2/auth_helpers.py | 58 ++
pydrive2/storage.py | 146 +++++
pydrive2/test/settings/default_user.yaml | 11 +
.../settings/default_user_no_refresh.yaml | 9 +
pydrive2/test/test_oauth.py | 28 +-
pydrive2/test/test_token_expiry.py | 77 +++
7 files changed, 616 insertions(+), 319 deletions(-)
create mode 100644 pydrive2/auth_helpers.py
create mode 100644 pydrive2/storage.py
create mode 100644 pydrive2/test/settings/default_user.yaml
create mode 100644 pydrive2/test/settings/default_user_no_refresh.yaml
create mode 100644 pydrive2/test/test_token_expiry.py
Index: PyDrive2-1.16.2/pydrive2/auth.py
===================================================================
--- PyDrive2-1.16.2.orig/pydrive2/auth.py
+++ PyDrive2-1.16.2/pydrive2/auth.py
@@ -1,28 +1,27 @@
import json
-import webbrowser
import httplib2
-import oauth2client.clientsecrets as clientsecrets
import threading
from googleapiclient.discovery import build
from functools import wraps
-from oauth2client.service_account import ServiceAccountCredentials
-from oauth2client.client import FlowExchangeError
-from oauth2client.client import AccessTokenRefreshError
-from oauth2client.client import OAuth2WebServerFlow
-from oauth2client.client import OOB_CALLBACK_URN
-from oauth2client.contrib.dictionary_storage import DictionaryStorage
-from oauth2client.file import Storage
-from oauth2client.tools import ClientRedirectHandler
-from oauth2client.tools import ClientRedirectServer
-from oauth2client._helpers import scopes_to_string
-from .apiattr import ApiAttribute
-from .apiattr import ApiAttributeMixin
+import google.oauth2.credentials
+import google.oauth2.service_account
+from .storage import FileBackend, DictionaryBackend
+
from .settings import LoadSettingsFile
from .settings import ValidateSettings
from .settings import SettingsError
from .settings import InvalidConfigError
+from .auth_helpers import verify_client_config
+from oauthlib.oauth2.rfc6749.errors import OAuth2Error, MissingCodeError
+from google_auth_oauthlib.flow import InstalledAppFlow
+from google_auth_httplib2 import AuthorizedHttp
+from warnings import warn
+
+
+_CLIENT_AUTH_PROMPT_MESSAGE = "Please visit this URL:\n{url}\n"
+
class AuthError(Exception):
"""Base error for authentication/authorization errors."""
@@ -45,23 +44,16 @@ class RefreshError(AuthError):
def LoadAuth(decoratee):
- """Decorator to check if the auth is valid and loads auth if not."""
+ """
+ Decorator to check the self.auth & self.http object in a decorated API call.
+ Loads a new GoogleAuth or Http object if needed.
+ """
@wraps(decoratee)
def _decorated(self, *args, **kwargs):
# Initialize auth if needed.
if self.auth is None:
self.auth = GoogleAuth()
- # Re-create access token if it expired.
- if self.auth.access_token_expired:
- if getattr(self.auth, "auth_method", False) == "service":
- self.auth.ServiceAuth()
- else:
- self.auth.LocalWebserverAuth()
-
- # Initialise service if not built yet.
- if self.auth.service is None:
- self.auth.Authorize()
# Ensure that a thread-safe HTTP object is provided.
if (
@@ -71,77 +63,20 @@ def LoadAuth(decoratee):
and "http" in kwargs["param"]
and kwargs["param"]["http"] is not None
):
+ # overwrites the HTTP objects used by the Gdrive API object
self.http = kwargs["param"]["http"]
del kwargs["param"]["http"]
else:
- # If HTTP object not specified, create or resuse an HTTP
- # object from the thread local storage.
- if not getattr(self.auth.thread_local, "http", None):
- self.auth.thread_local.http = self.auth.Get_Http_Object()
- self.http = self.auth.thread_local.http
+ # If HTTP object not specified, resuse HTTP from self.auth.thread_local
+ self.http = self.auth.authorized_http
return decoratee(self, *args, **kwargs)
return _decorated
-def CheckServiceAuth(decoratee):
- """Decorator to authorize service account."""
-
- @wraps(decoratee)
- def _decorated(self, *args, **kwargs):
- self.auth_method = "service"
- dirty = False
- save_credentials = self.settings.get("save_credentials")
- if self.credentials is None and save_credentials:
- self.LoadCredentials()
- if self.credentials is None:
- decoratee(self, *args, **kwargs)
- self.Authorize()
- dirty = True
- elif self.access_token_expired:
- self.Refresh()
- dirty = True
- self.credentials.set_store(self._default_storage)
- if dirty and save_credentials:
- self.SaveCredentials()
-
- return _decorated
-
-
-def CheckAuth(decoratee):
- """Decorator to check if it requires OAuth2 flow request."""
-
- @wraps(decoratee)
- def _decorated(self, *args, **kwargs):
- dirty = False
- code = None
- save_credentials = self.settings.get("save_credentials")
- if self.credentials is None and save_credentials:
- self.LoadCredentials()
- if self.flow is None:
- self.GetFlow()
- if self.credentials is None:
- code = decoratee(self, *args, **kwargs)
- dirty = True
- else:
- if self.access_token_expired:
- if self.credentials.refresh_token is not None:
- self.Refresh()
- else:
- code = decoratee(self, *args, **kwargs)
- dirty = True
- if code is not None:
- self.Auth(code)
- self.credentials.set_store(self._default_storage)
- if dirty and save_credentials:
- self.SaveCredentials()
-
- return _decorated
-
-
-class GoogleAuth(ApiAttributeMixin):
+class GoogleAuth:
"""Wrapper class for oauth2client library in google-api-python-client.
Loads all settings and credentials from one 'settings.yaml' file
@@ -155,37 +90,24 @@ class GoogleAuth(ApiAttributeMixin):
"save_credentials": False,
"oauth_scope": ["https://www.googleapis.com/auth/drive"],
}
- CLIENT_CONFIGS_LIST = [
- "client_id",
- "client_secret",
- "auth_uri",
- "token_uri",
- "revoke_uri",
- "redirect_uri",
- ]
+
SERVICE_CONFIGS_LIST = ["client_user_email"]
- settings = ApiAttribute("settings")
- client_config = ApiAttribute("client_config")
- flow = ApiAttribute("flow")
- credentials = ApiAttribute("credentials")
- http = ApiAttribute("http")
- service = ApiAttribute("service")
- auth_method = ApiAttribute("auth_method")
def __init__(
self, settings_file="settings.yaml", http_timeout=None, settings=None
):
"""Create an instance of GoogleAuth.
+ This constructor parses just the yaml settings file.
+ All other config & auth related objects are lazily loaded (see properties section)
+
:param settings_file: path of settings file. 'settings.yaml' by default.
:type settings_file: str.
:param settings: settings dict.
:type settings: dict.
"""
self.http_timeout = http_timeout
- ApiAttributeMixin.__init__(self)
self.thread_local = threading.local()
- self.client_config = {}
if settings is None and settings_file:
try:
@@ -196,21 +118,92 @@ class GoogleAuth(ApiAttributeMixin):
self.settings = settings or self.DEFAULT_SETTINGS
ValidateSettings(self.settings)
- storages, default = self._InitializeStoragesFromSettings()
- self._storages = storages
- self._default_storage = default
+ self._service = None
+ self._client_config = None
+ self._oauth_type = None
+ self._flow = None
+ self._storage_registry = {}
+ self._default_storage = None
+ self._credentials = None
+
+ @property
+ def service(self):
+ if not self._service:
+ self._service = build("drive", "v2", cache_discovery=False)
+ return self._service
+
+ @property
+ def client_config(self):
+ if not self._client_config:
+ self.LoadClientConfig()
+ return self._client_config
+
+ @property
+ def oauth_type(self):
+ if not self._oauth_type:
+ self.LoadClientConfig()
+ return self._oauth_type
+
+ @property
+ def flow(self):
+ if not self._flow:
+ self.GetFlow()
+ return self._flow
+
+ @property
+ def default_storage(self):
+ if not self.settings.get("save_credentials"):
+ return None
+
+ if not self._default_storage:
+ self._InitializeStoragesFromSettings()
+ return self._default_storage
@property
+ def credentials(self):
+ if not self._credentials:
+ if self.oauth_type in ("web", "installed"):
+ # try to load from backend if available
+ # credentials would auto-refresh if expired
+ if self.default_storage:
+ try:
+ self.LoadCredentials()
+ return self._credentials
+ except FileNotFoundError:
+ pass
+
+ self.LocalWebserverAuth()
+
+ elif self.oauth_type == "service":
+ self.ServiceAuth()
+ else:
+ raise InvalidConfigError(
+ "Only web, installed, service oauth is supported"
+ )
+
+ return self._credentials
+
+ @property
+ def authorized_http(self):
+ # returns a thread-safe, local, cached HTTP object
+ if not getattr(self.thread_local, "http", None):
+ # If HTTP object not available in thread_local,
+ # create and store Authorized Http object in thread_local storage
+ self.thread_local.http = self.Get_Http_Object()
+
+ return self.thread_local.http
+
+ # Other properties
+ @property
def access_token_expired(self):
"""Checks if access token doesn't exist or is expired.
:returns: bool -- True if access token doesn't exist or is expired.
"""
- if self.credentials is None:
+ if not self.credentials:
return True
- return self.credentials.access_token_expired
+ return not self.credentials.valid
- @CheckAuth
def LocalWebserverAuth(
self,
host_name="localhost",
@@ -238,119 +231,100 @@ class GoogleAuth(ApiAttributeMixin):
:raises: AuthenticationRejected, AuthenticationError
"""
if port_numbers is None:
- port_numbers = [
- 8080,
- 8090,
- ] # Mutable objects should not be default
- # values, as each call's changes are global.
- success = False
+ port_numbers = [8080, 8090]
+
+ additional_config = {}
+ # offline token request needed to obtain refresh token
+ # make sure that consent is requested
+ if self.settings.get("get_refresh_token"):
+ additional_config["access_type"] = "offline"
+ additional_config["prompt"] = "select_account"
+
port_number = 0
for port in port_numbers:
port_number = port
try:
- httpd = ClientRedirectServer(
- (bind_addr or host_name, port), ClientRedirectHandler
+ self._credentials = self.flow.run_local_server(
+ host=bind_addr or host_name,
+ port=port_number,
+ authorization_prompt_message=_CLIENT_AUTH_PROMPT_MESSAGE,
+ open_browser=launch_browser,
+ **additional_config,
+ )
+ except OSError as e:
+ print(
+ "Port {} is in use. Trying a different port".format(port)
)
- except OSError:
- pass
- else:
- success = True
- break
- if success:
- oauth_callback = f"http://{host_name}:{port_number}/"
- else:
- print(
- "Failed to start a local web server. Please check your firewall"
- )
- print(
- "settings and locally running programs that may be blocking or"
- )
- print("using configured ports. Default ports are 8080 and 8090.")
- raise AuthenticationError()
- self.flow.redirect_uri = oauth_callback
- authorize_url = self.GetAuthUrl()
- if launch_browser:
- webbrowser.open(authorize_url, new=1, autoraise=True)
- print("Your browser has been opened to visit:")
- else:
- print("Open your browser to visit:")
- print()
- print(" " + authorize_url)
- print()
- httpd.handle_request()
- if "error" in httpd.query_params:
- print("Authentication request was rejected")
- raise AuthenticationRejected("User rejected authentication")
- if "code" in httpd.query_params:
- return httpd.query_params["code"]
- else:
- print(
- 'Failed to find "code" in the query parameters of the redirect.'
- )
- print("Try command-line authentication")
- raise AuthenticationError("No code found in redirect")
- @CheckAuth
+ except MissingCodeError as e:
+ # if code is not found in the redirect uri's query parameters
+ print(
+ "Failed to find 'code' in the query parameters of the redirect."
+ )
+ print("Please check that your redirect uri is correct.")
+ raise AuthenticationError("No code found in redirect uri")
+
+ except OAuth2Error as e:
+ # catch all other oauth 2 errors
+ print("Authentication request was rejected")
+ raise AuthenticationRejected("User rejected authentication")
+
+ # if any port results in successful auth, we're done
+ if self._credentials:
+ if self.default_storage:
+ self.SaveCredentials()
+
+ return
+
+ # If we have tried all ports and could not find a port
+ print("Failed to start a local web server. Please check your firewall")
+ print("settings and locally running programs that may be blocking or")
+ print("using configured ports. Default ports are 8080 and 8090.")
+ raise AuthenticationError("None of the specified ports are available")
+
def CommandLineAuth(self):
"""Authenticate and authorize from user by printing authentication url
retrieving authentication code from command-line.
:returns: str -- code returned from commandline.
"""
- self.flow.redirect_uri = OOB_CALLBACK_URN
- authorize_url = self.GetAuthUrl()
- print("Go to the following link in your browser:")
- print()
- print(" " + authorize_url)
- print()
- return input("Enter verification code: ").strip()
+ raise DeprecationWarning(
+ "The command line auth has been deprecated. "
+ "The recommended alternative is to use local webserver auth with a loopback address."
+ )
- @CheckServiceAuth
def ServiceAuth(self):
"""Authenticate and authorize using P12 private key, client id
and client email for a Service account.
:raises: AuthError, InvalidConfigError
"""
- if set(self.SERVICE_CONFIGS_LIST) - set(self.client_config):
- self.LoadServiceConfigSettings()
- scopes = scopes_to_string(self.settings["oauth_scope"])
keyfile_name = self.client_config.get("client_json_file_path")
keyfile_dict = self.client_config.get("client_json_dict")
keyfile_json = self.client_config.get("client_json")
+ # setting the subject for domain-wide delegation
+ additional_config = {}
+ additional_config["subject"] = self.client_config.get(
+ "client_user_email"
+ )
+ additional_config["scopes"] = self.settings["oauth_scope"]
+
if not keyfile_dict and keyfile_json:
# Compensating for missing ServiceAccountCredentials.from_json_keyfile
keyfile_dict = json.loads(keyfile_json)
if keyfile_dict:
- self.credentials = (
- ServiceAccountCredentials.from_json_keyfile_dict(
- keyfile_dict=keyfile_dict, scopes=scopes
- )
+ self._credentials = google.oauth2.service_account.Credentials.from_service_account_info(
+ keyfile_dict, **additional_config
)
elif keyfile_name:
- self.credentials = (
- ServiceAccountCredentials.from_json_keyfile_name(
- filename=keyfile_name, scopes=scopes
- )
+ self._credentials = google.oauth2.service_account.Credentials.from_service_account_file(
+ keyfile_name, **additional_config
)
else:
- service_email = self.client_config["client_service_email"]
- file_path = self.client_config["client_pkcs12_file_path"]
- self.credentials = ServiceAccountCredentials.from_p12_keyfile(
- service_account_email=service_email,
- filename=file_path,
- scopes=scopes,
- )
-
- user_email = self.client_config.get("client_user_email")
- if user_email:
- self.credentials = self.credentials.create_delegated(
- sub=user_email
- )
+ raise AuthenticationError("Invalid service credentials")
def _InitializeStoragesFromSettings(self):
- result = {"file": None, "dictionary": None}
backend = self.settings.get("save_credentials_backend")
save_credentials = self.settings.get("save_credentials")
if backend == "file":
@@ -359,7 +333,9 @@ class GoogleAuth(ApiAttributeMixin):
raise InvalidConfigError(
"Please specify credentials file to read"
)
- result[backend] = Storage(credentials_file)
+
+ self._storage_registry[backend] = FileBackend(credentials_file)
+
elif backend == "dictionary":
creds_dict = self.settings.get("save_credentials_dict")
if creds_dict is None:
@@ -369,12 +345,14 @@ class GoogleAuth(ApiAttributeMixin):
if creds_key is None:
raise InvalidConfigError("Please specify credentials key")
- result[backend] = DictionaryStorage(creds_dict, creds_key)
+ self._storage_registry[backend] = DictionaryBackend(creds_dict)
+
elif save_credentials:
raise InvalidConfigError(
"Unknown save_credentials_backend: %s" % backend
)
- return result, result.get(backend)
+
+ self._default_storage = self._storage_registry.get(backend)
def LoadCredentials(self, backend=None):
"""Loads credentials or create empty credentials if it doesn't exist.
@@ -404,7 +382,8 @@ class GoogleAuth(ApiAttributeMixin):
:raises: InvalidConfigError, InvalidCredentialsError
"""
if credentials_file is None:
- self._default_storage = self._storages["file"]
+ self._default_storage = self._storage_registry["file"]
+ credentials_file = self.settings.get("save_credentials_file")
if self._default_storage is None:
raise InvalidConfigError(
"Backend `file` is not configured, specify "
@@ -412,30 +391,49 @@ class GoogleAuth(ApiAttributeMixin):
"file or pass an explicit value"
)
else:
- self._default_storage = Storage(credentials_file)
+ self._default_storage = FileBackend(credentials_file)
try:
- self.credentials = self._default_storage.get()
+ auth_info = self.default_storage.read_credentials()
+ except FileNotFoundError:
+ # if credential was not found, raise the error for handling
+ raise
except OSError:
+ # catch other errors
raise InvalidCredentialsError(
"Credentials file cannot be symbolic link"
)
- if self.credentials:
- self.credentials.set_store(self._default_storage)
+ try:
+ self._credentials = google.oauth2.credentials.Credentials.from_authorized_user_info(
+ auth_info, scopes=auth_info["scopes"]
+ )
+ except ValueError:
+ warn(
+ "Loading authorized user credentials without a refresh token is "
+ "not officially supported by google auth library. We recommend that "
+ "you only store refreshable credentials moving forward."
+ )
+
+ self._credentials = google.oauth2.credentials.Credentials(
+ token=auth_info.get("token"),
+ token_uri="https://oauth2.googleapis.com/token", # always overrides
+ scopes=auth_info.get("scopes"),
+ client_id=auth_info.get("client_id"),
+ client_secret=auth_info.get("client_secret"),
+ )
def _LoadCredentialsDictionary(self):
- self._default_storage = self._storages["dictionary"]
+ self._default_storage = self._storage_registry["dictionary"]
if self._default_storage is None:
raise InvalidConfigError(
"Backend `dictionary` is not configured, specify "
"credentials dict and key to read in the settings file"
)
- self.credentials = self._default_storage.get()
+ creds_key = self.settings.get("save_credentials_key")
- if self.credentials:
- self.credentials.set_store(self._default_storage)
+ self._credentials = self.default_storage.read_credentials(creds_key)
def SaveCredentials(self, backend=None):
"""Saves credentials according to specified backend.
@@ -465,39 +463,39 @@ class GoogleAuth(ApiAttributeMixin):
:type credentials_file: str.
:raises: InvalidConfigError, InvalidCredentialsError
"""
- if self.credentials is None:
+ if self._credentials is None:
raise InvalidCredentialsError("No credentials to save")
if credentials_file is None:
- storage = self._storages["file"]
- if storage is None:
+ credentials_file = self.settings.get("save_credentials_file")
+ if credentials_file is None:
raise InvalidConfigError(
- "Backend `file` is not configured, specify "
- "credentials file to read in the settings "
- "file or pass an explicit value"
+ "Please specify credentials file to read"
)
- else:
- storage = Storage(credentials_file)
+
+ storage = self._storage_registry["file"]
try:
- storage.put(self.credentials)
+ storage.store_credentials(self._credentials)
+
except OSError:
raise InvalidCredentialsError(
"Credentials file cannot be symbolic link"
)
def _SaveCredentialsDictionary(self):
- if self.credentials is None:
+ if self._credentials is None:
raise InvalidCredentialsError("No credentials to save")
- storage = self._storages["dictionary"]
+ storage = self._storage_registry["dictionary"]
if storage is None:
raise InvalidConfigError(
"Backend `dictionary` is not configured, specify "
"credentials dict and key to write in the settings file"
)
- storage.put(self.credentials)
+ creds_key = self.settings.get("save_credentials_key")
+ storage.store_credentials(self._credentials, creds_key)
def LoadClientConfig(self, backend=None):
"""Loads client configuration according to specified backend.
@@ -535,45 +533,18 @@ class GoogleAuth(ApiAttributeMixin):
"""
if client_config_file is None:
client_config_file = self.settings["client_config_file"]
- try:
- client_type, client_info = clientsecrets.loadfile(
- client_config_file
- )
- except clientsecrets.InvalidClientSecretsError as error:
- raise InvalidConfigError("Invalid client secrets file %s" % error)
- if client_type not in (
- clientsecrets.TYPE_WEB,
- clientsecrets.TYPE_INSTALLED,
- ):
- raise InvalidConfigError(
- "Unknown client_type of client config file"
- )
- # General settings.
- try:
- config_index = [
- "client_id",
- "client_secret",
- "auth_uri",
- "token_uri",
- ]
- for config in config_index:
- self.client_config[config] = client_info[config]
-
- self.client_config["revoke_uri"] = client_info.get("revoke_uri")
- self.client_config["redirect_uri"] = client_info["redirect_uris"][
- 0
- ]
- except KeyError:
- raise InvalidConfigError("Insufficient client config in file")
+ with open(client_config_file, "r") as json_file:
+ client_config = json.load(json_file)
- # Service auth related fields.
- service_auth_config = ["client_email"]
try:
- for config in service_auth_config:
- self.client_config[config] = client_info[config]
- except KeyError:
- pass # The service auth fields are not present, handling code can go here.
+ # check the format of the loaded client config
+ client_type, checked_config = verify_client_config(client_config)
+ except ValueError as error:
+ raise InvalidConfigError("Invalid client secrets file: %s" % error)
+
+ self._client_config = checked_config
+ self._oauth_type = client_type
def LoadServiceConfigSettings(self):
"""Loads client configuration from settings.
@@ -585,11 +556,12 @@ class GoogleAuth(ApiAttributeMixin):
"client_json",
"client_pkcs12_file_path",
]
+ service_config = {}
for config in configs:
value = self.settings["service_config"].get(config)
if value:
- self.client_config[config] = value
+ service_config[config] = value
break
else:
raise InvalidConfigError(
@@ -597,91 +569,91 @@ class GoogleAuth(ApiAttributeMixin):
)
if config == "client_pkcs12_file_path":
- self.SERVICE_CONFIGS_LIST.append("client_service_email")
+ # see https://github.com/googleapis/google-auth-library-python/issues/288
+ raise DeprecationWarning(
+ "PKCS#12 files are no longer supported in the new google.auth library. "
+ "Please download a new json service credential file from google cloud console. "
+ "For more info, visit https://github.com/googleapis/google-auth-library-python/issues/288"
+ )
for config in self.SERVICE_CONFIGS_LIST:
try:
- self.client_config[config] = self.settings["service_config"][
+ service_config[config] = self.settings["service_config"].get(
config
- ]
+ )
except KeyError:
err = "Insufficient service config in settings"
err += f"\n\nMissing: {config} key."
raise InvalidConfigError(err)
+ self._client_config = service_config
+ self._oauth_type = "service"
+
def LoadClientConfigSettings(self):
"""Loads client configuration from settings file.
:raises: InvalidConfigError
"""
- for config in self.CLIENT_CONFIGS_LIST:
- try:
- self.client_config[config] = self.settings["client_config"][
- config
- ]
- except KeyError:
- raise InvalidConfigError(
- "Insufficient client config in settings"
- )
+ try:
+ client_config = self.settings["client_config"]
+ except KeyError as e:
+ raise InvalidConfigError(
+ "Settings does not contain 'client_config'"
+ )
+
+ try:
+ _, checked_config = verify_client_config(
+ client_config, with_oauth_type=False
+ )
+ except ValueError as e:
+ raise InvalidConfigError("Invalid client secrets file: %s" % e)
- def GetFlow(self):
+ # assumed to be Installed App Flow as the Local Server Auth is appropriate for this type of device
+ self._client_config = checked_config
+ self._oauth_type = "installed"
+
+ def GetFlow(self, scopes=None, **kwargs):
"""Gets Flow object from client configuration.
:raises: InvalidConfigError
"""
- if not all(
- config in self.client_config for config in self.CLIENT_CONFIGS_LIST
- ):
- self.LoadClientConfig()
- constructor_kwargs = {
- "redirect_uri": self.client_config["redirect_uri"],
- "auth_uri": self.client_config["auth_uri"],
- "token_uri": self.client_config["token_uri"],
- "access_type": "online",
- }
- if self.client_config["revoke_uri"] is not None:
- constructor_kwargs["revoke_uri"] = self.client_config["revoke_uri"]
- self.flow = OAuth2WebServerFlow(
- self.client_config["client_id"],
- self.client_config["client_secret"],
- scopes_to_string(self.settings["oauth_scope"]),
- **constructor_kwargs,
- )
- if self.settings.get("get_refresh_token"):
- self.flow.params.update(
- {"access_type": "offline", "approval_prompt": "force"}
+ if not scopes:
+ scopes = self.settings.get("oauth_scope")
+
+ if self.oauth_type in ("web", "installed"):
+ self._flow = InstalledAppFlow.from_client_config(
+ {self.oauth_type: self.client_config},
+ scopes=scopes,
+ **kwargs,
)
+ if self.oauth_type == "service":
+ # In a service oauth2 flow,
+ # the oauth subject does not have to provide any consent via the client
+ pass
+
def Refresh(self):
"""Refreshes the access_token.
-
:raises: RefreshError
"""
- if self.credentials is None:
- raise RefreshError("No credential to refresh.")
- if (
- self.credentials.refresh_token is None
- and self.auth_method != "service"
- ):
- raise RefreshError(
- "No refresh_token found."
- "Please set access_type of OAuth to offline."
- )
- if self.http is None:
- self.http = self._build_http()
- try:
- self.credentials.refresh(self.http)
- except AccessTokenRefreshError as error:
- raise RefreshError("Access token refresh failed: %s" % error)
+ raise DeprecationWarning(
+ "Manual refresh is deprecated as the"
+ "new google auth library handles refresh automatically"
+ )
- def GetAuthUrl(self):
+ def GetAuthUrl(self, redirect_uri="http://localhost:8080/"):
"""Creates authentication url where user visits to grant access.
:returns: str -- Authentication url.
"""
- if self.flow is None:
- self.GetFlow()
- return self.flow.step1_get_authorize_url()
+ if self.oauth_type == "service":
+ raise AuthenticationError(
+ "Authentication is not required for service client type."
+ )
+
+ self.flow.redirect_uri = redirect_uri
+
+ return self.flow.authorization_url()
def Auth(self, code):
"""Authenticate, authorize, and build service.
@@ -691,7 +663,6 @@ class GoogleAuth(ApiAttributeMixin):
:raises: AuthenticationError
"""
self.Authenticate(code)
- self.Authorize()
def Authenticate(self, code):
"""Authenticates given authentication code back from user.
@@ -700,12 +671,35 @@ class GoogleAuth(ApiAttributeMixin):
:type code: str.
:raises: AuthenticationError
"""
- if self.flow is None:
- self.GetFlow()
+ from urllib.parse import unquote
+
+ if self.oauth_type == "service":
+ raise AuthenticationError(
+ "Authentication is not required for service client type."
+ )
+
try:
- self.credentials = self.flow.step2_exchange(code)
- except FlowExchangeError as e:
- raise AuthenticationError("OAuth2 code exchange failed: %s" % e)
+ self.flow.fetch_token(code=unquote(code))
+
+ except MissingCodeError as e:
+ # if code is not found in the redirect uri's query parameters
+ print(
+ "Failed to find 'code' in the query parameters of the redirect."
+ )
+ print("Please check that your redirect uri is correct.")
+ raise AuthenticationError("No code found in redirect")
+
+ except OAuth2Error as e:
+ # catch oauth 2 errors
+ print("Authentication request was rejected")
+ raise AuthenticationRejected("User rejected authentication") from e
+
+ self._credentials = self.flow.credentials
+
+ # save credentials if there's a default storage
+ if self.default_storage:
+ self.SaveCredentials()
+
print("Authentication successful.")
def _build_http(self):
@@ -728,24 +722,16 @@ class GoogleAuth(ApiAttributeMixin):
:raises: AuthenticationError
"""
- if self.access_token_expired:
- raise AuthenticationError(
- "No valid credentials provided to authorize"
- )
-
- if self.http is None:
- self.http = self._build_http()
- self.http = self.credentials.authorize(self.http)
- self.service = build(
- "drive", "v2", http=self.http, cache_discovery=False
+ raise DeprecationWarning(
+ "Manual authorization of HTTP will be deprecated as the"
+ "new google auth library handles the adding to relevant oauth headers automatically"
)
def Get_Http_Object(self):
- """Create and authorize an httplib2.Http object. Necessary for
- thread-safety.
+ """
+ Helper function to get a new Authorized Http object.
:return: The http object to be used in each call.
:rtype: httplib2.Http
"""
- http = self._build_http()
- http = self.credentials.authorize(http)
- return http
+
+ return AuthorizedHttp(self.credentials, http=self._build_http())
Index: PyDrive2-1.16.2/pydrive2/auth_helpers.py
===================================================================
--- /dev/null
+++ PyDrive2-1.16.2/pydrive2/auth_helpers.py
@@ -0,0 +1,58 @@
+_OLD_CLIENT_CONFIG_KEYS = frozenset(
+ (
+ "client_id",
+ "client_secret",
+ "auth_uri",
+ "token_uri",
+ "revoke_uri",
+ "redirect_uri",
+ )
+)
+
+_CLIENT_CONFIG_KEYS = frozenset(
+ (
+ "client_id",
+ "client_secret",
+ "auth_uri",
+ "token_uri",
+ "redirect_uris",
+ )
+)
+
+
+def verify_client_config(client_config, with_oauth_type=True):
+ """Verifies that format of the client config
+ loaded from a Google-format client secrets file.
+ """
+
+ oauth_type = None
+ config = client_config
+
+ if with_oauth_type:
+ if "web" in client_config:
+ oauth_type = "web"
+ config = config["web"]
+
+ elif "installed" in client_config:
+ oauth_type = "installed"
+ config = config["installed"]
+ else:
+ raise ValueError(
+ "Client secrets must be for a web or installed app"
+ )
+
+ # This is the older format of client config
+ if _OLD_CLIENT_CONFIG_KEYS.issubset(config.keys()):
+ config["redirect_uris"] = [config["redirect_uri"]]
+
+ # by default, the redirect uri is the first in the list
+ if "redirect_uri" not in config:
+ config["redirect_uri"] = config["redirect_uris"][0]
+
+ if "revoke_uri" not in config:
+ config["revoke_uri"] = "https://oauth2.googleapis.com/revoke"
+
+ if not _CLIENT_CONFIG_KEYS.issubset(config.keys()):
+ raise ValueError("Client secrets is not in the correct format.")
+
+ return oauth_type, config
Index: PyDrive2-1.16.2/pydrive2/storage.py
===================================================================
--- /dev/null
+++ PyDrive2-1.16.2/pydrive2/storage.py
@@ -0,0 +1,116 @@
+import os
+import json
+import warnings
+import threading
+
+
+_SYM_LINK_MESSAGE = "File: {0}: Is a symbolic link."
+_IS_DIR_MESSAGE = "{0}: Is a directory"
+_MISSING_FILE_MESSAGE = "Cannot access {0}: No such file or directory"
+
+
+def validate_file(filename):
+ if os.path.islink(filename):
+ raise IOError(_SYM_LINK_MESSAGE.format(filename))
+ elif os.path.isdir(filename):
+ raise IOError(_IS_DIR_MESSAGE.format(filename))
+ elif not os.path.isfile(filename):
+ warnings.warn(_MISSING_FILE_MESSAGE.format(filename))
+
+
+class CredentialBackend(object):
+ """Adapter that provides a consistent interface to read and write credentials"""
+
+ def _read_credentials(self, **kwargs):
+ """Specific implementation of how credentials are retrieved from backend"""
+ return NotImplementedError
+
+ def _store_credentials(self, credential, **kwargs):
+ """Specific implementation of how credentials are written to backend"""
+ return NotImplementedError
+
+ def _delete_credentials(self, **kwargs):
+ """Specific implementation of how credentials are deleted from backend"""
+ return NotImplementedError
+
+ def read_credentials(self, **kwargs):
+ """Reads a credential config from the backend and
+ returns the config as a dictionary
+ :return: A dictionary of the credentials
+ """
+ return self._read_credentials(**kwargs)
+
+ def store_credentials(self, credential, **kwargs):
+ """Write a credential to the backend"""
+ self._store_credentials(credential, **kwargs)
+
+ def delete_credentials(self, **kwargs):
+ """Delete credential.
+ Frees any resources associated with storing the credential
+ """
+ self._delete_credentials(**kwargs)
+
+
+class FileBackend(CredentialBackend):
+ """Read and write credential to a specific file backend with Thread-locking"""
+
+ def __init__(self, filename):
+ self._filename = filename
+ self._thread_lock = threading.Lock()
+
+ def _create_file_if_needed(self, filename):
+ """Create an empty file if necessary.
+ This method will not initialize the file. Instead it implements a
+ simple version of "touch" to ensure the file has been created.
+ """
+ if not os.path.exists(filename):
+ old_umask = os.umask(0o177)
+ try:
+ open(filename, "a+b").close()
+ finally:
+ os.umask(old_umask)
+
+ def _read_credentials(self, **kwargs):
+ """Reads a local json file and parses the information into a info dictionary."""
+ with self._thread_lock:
+ validate_file(self._filename)
+ with open(self._filename, "r") as json_file:
+ return json.load(json_file)
+
+ def _store_credentials(self, credentials, **kwargs):
+ """Writes current credentials to a local json file."""
+ with self._thread_lock:
+ # write new credentials to the temp file
+ dirname, filename = os.path.split(self._filename)
+ temp_path = os.path.join(dirname, "temp_{}".format(filename))
+ self._create_file_if_needed(temp_path)
+
+ with open(temp_path, "w") as json_file:
+ json_file.write(credentials.to_json())
+
+ # replace the existing credential file
+ os.replace(temp_path, self._filename)
+
+ def _delete_credentials(self, **kwargs):
+ """Delete credentials file."""
+ with self._thread_lock:
+ os.unlink(self._filename)
+
+
+class DictionaryBackend(CredentialBackend):
+ """Read and write credentials to a dictionary backend"""
+
+ def __init__(self, dictionary):
+ self._dictionary = dictionary
+
+ def _read_credentials(self, key):
+ """Reads a local json file and parses the information into a info dictionary."""
+ return self._dictionary.get(key)
+
+ def _store_credentials(self, credentials, key):
+ """Writes current credentials to a local json file."""
+ self._dictionary[key] = credentials.to_json()
+
+ def _delete_credentials(self, key):
+ """Delete Credentials file."""
+ self._dictionary.pop(key, None)
Index: PyDrive2-1.16.2/pydrive2/test/settings/default_user.yaml
===================================================================
--- /dev/null
+++ PyDrive2-1.16.2/pydrive2/test/settings/default_user.yaml
@@ -0,0 +1,11 @@
+client_config_backend: file
+client_config_file: /tmp/pydrive2/user.json
+
+save_credentials: True
+save_credentials_backend: file
+save_credentials_file: credentials/default_user.dat
+
+oauth_scope:
+ - https://www.googleapis.com/auth/drive
+
+get_refresh_token: True
\ No newline at end of file
Index: PyDrive2-1.16.2/pydrive2/test/settings/default_user_no_refresh.yaml
===================================================================
--- /dev/null
+++ PyDrive2-1.16.2/pydrive2/test/settings/default_user_no_refresh.yaml
@@ -0,0 +1,9 @@
+client_config_backend: file
+client_config_file: /tmp/pydrive2/user.json
+
+save_credentials: True
+save_credentials_backend: file
+save_credentials_file: credentials/default_user_no_refresh.dat
+
+oauth_scope:
+ - https://www.googleapis.com/auth/drive
Index: PyDrive2-1.16.2/pydrive2/test/test_oauth.py
===================================================================
--- PyDrive2-1.16.2.orig/pydrive2/test/test_oauth.py
+++ PyDrive2-1.16.2/pydrive2/test/test_oauth.py
@@ -10,7 +10,7 @@ from pydrive2.test.test_util import (
settings_file_path,
GDRIVE_USER_CREDENTIALS_DATA,
)
-from oauth2client.file import Storage
+from ..storage import FileBackend
def setup_module(module):
@@ -24,6 +24,7 @@ def test_01_LocalWebserverAuthWithClient
# Test if authentication works with config read from file
ga = GoogleAuth(settings_file_path("test_oauth_test_01.yaml"))
ga.LocalWebserverAuth()
+ assert ga.credentials
assert not ga.access_token_expired
# Test if correct credentials file is created
CheckCredentialsFile("credentials/1.dat")
@@ -37,6 +38,7 @@ def test_02_LocalWebserverAuthWithClient
# Test if authentication works with config read from settings
ga = GoogleAuth(settings_file_path("test_oauth_test_02.yaml"))
ga.LocalWebserverAuth()
+ assert ga.credentials
assert not ga.access_token_expired
# Test if correct credentials file is created
CheckCredentialsFile("credentials/2.dat")
@@ -50,6 +52,7 @@ def test_03_LocalWebServerAuthWithNoCred
ga = GoogleAuth(settings_file_path("test_oauth_test_03.yaml"))
assert not ga.settings["save_credentials"]
ga.LocalWebserverAuth()
+ assert ga.credentials
assert not ga.access_token_expired
time.sleep(1)
@@ -61,6 +64,7 @@ def test_04_CommandLineAuthWithClientCon
# Test if authentication works with config read from file
ga = GoogleAuth(settings_file_path("test_oauth_test_04.yaml"))
ga.CommandLineAuth()
+ assert ga.credentials
assert not ga.access_token_expired
# Test if correct credentials file is created
CheckCredentialsFile("credentials/4.dat")
@@ -72,6 +76,7 @@ def test_05_ConfigFromSettingsWithoutOau
# Test if authentication works without oauth_scope
ga = GoogleAuth(settings_file_path("test_oauth_test_05.yaml"))
ga.LocalWebserverAuth()
+ assert ga.credentials
assert not ga.access_token_expired
time.sleep(1)
@@ -81,7 +86,7 @@ def test_06_ServiceAuthFromSavedCredenti
setup_credentials("credentials/6.dat")
ga = GoogleAuth(settings_file_path("test_oauth_test_06.yaml"))
ga.ServiceAuth()
- assert not ga.access_token_expired
+ assert ga.credentials
time.sleep(1)
@@ -92,13 +97,14 @@ def test_07_ServiceAuthFromSavedCredenti
# Delete old credentials file
delete_file(credentials_file)
assert not os.path.exists(credentials_file)
+ # For Service Auth, credentials are created with no token (treated as expired)
+ # JWT token is not populated until the first request where auto-refresh happens
+ # assert not ga.access_token_expired
ga.ServiceAuth()
- assert os.path.exists(credentials_file)
- # Secondary auth should be made only using the previously saved
- # login info
+ assert ga.credentials
ga = GoogleAuth(settings_file_path("test_oauth_test_07.yaml"))
ga.ServiceAuth()
- assert not ga.access_token_expired
+ assert ga.credentials
time.sleep(1)
@@ -108,6 +114,7 @@ def test_08_ServiceAuthFromJsonFileNoCre
ga = GoogleAuth(settings_file_path("test_oauth_test_08.yaml"))
assert not ga.settings["save_credentials"]
ga.ServiceAuth()
+ assert ga.credentials
time.sleep(1)
@@ -120,10 +127,9 @@ def test_09_SaveLoadCredentialsUsesDefau
# Delete old credentials file
delete_file(credentials_file)
assert not os.path.exists(credentials_file)
- spy = mocker.spy(Storage, "__init__")
+ spy = mocker.spy(FileBackend, "__init__")
ga.ServiceAuth()
- ga.LoadCredentials()
- ga.SaveCredentials()
+ assert ga.credentials
assert spy.call_count == 0
@@ -142,14 +148,14 @@ def test_10_ServiceAuthFromSavedCredenti
}
ga = GoogleAuth(settings=settings)
ga.ServiceAuth()
- assert not ga.access_token_expired
+ assert ga.credentials
assert creds_dict
first_creds_dict = creds_dict.copy()
# Secondary auth should be made only using the previously saved
# login info
ga = GoogleAuth(settings=settings)
ga.ServiceAuth()
- assert not ga.access_token_expired
+ assert ga.credentials
assert creds_dict == first_creds_dict
time.sleep(1)
Index: PyDrive2-1.16.2/pydrive2/test/test_token_expiry.py
===================================================================
--- /dev/null
+++ PyDrive2-1.16.2/pydrive2/test/test_token_expiry.py
@@ -0,0 +1,77 @@
+import pytest
+from pydrive2.auth import GoogleAuth
+from pydrive2.drive import GoogleDrive
+from pydrive2.test.test_util import (
+ settings_file_path,
+ setup_credentials,
+ pydrive_retry,
+ delete_file,
+)
+from google.auth.exceptions import RefreshError
+
+
+@pytest.fixture
+def googleauth_refresh():
+ setup_credentials()
+ # Delete old credentials file
+ delete_file("credentials/default_user.dat")
+ ga = GoogleAuth(settings_file_path("default_user.yaml"))
+ ga.LocalWebserverAuth()
+
+ return ga
+
+
+@pytest.fixture
+def googleauth_no_refresh():
+ setup_credentials()
+ # Delete old credentials file
+ delete_file("credentials/default_user_no_refresh.dat")
+ ga = GoogleAuth(settings_file_path("default_user_no_refresh.yaml"))
+ ga.LocalWebserverAuth()
+
+ return ga
+
+
+@pytest.mark.manual
+def test_01_TokenExpiryWithRefreshToken(googleauth_refresh):
+ gdrive = GoogleDrive(googleauth_refresh)
+
+ about_object = pydrive_retry(gdrive.GetAbout)
+ assert about_object is not None
+
+ # save the first access token for comparison
+ token1 = gdrive.auth.credentials.token
+
+ # simulate token expiry by deleting the underlying token
+ gdrive.auth.credentials.token = None
+
+ # credential object should still exist but access token expired
+ assert gdrive.auth.credentials
+ assert gdrive.auth.access_token_expired
+
+ about_object = pydrive_retry(gdrive.GetAbout)
+ assert about_object is not None
+
+ # save the second access token for comparison
+ token2 = gdrive.auth.credentials.token
+
+ assert token1 != token2
+
+
+@pytest.mark.manual
+def test_02_TokenExpiryWithoutRefreshToken(googleauth_no_refresh):
+ gdrive = GoogleDrive(googleauth_no_refresh)
+
+ about_object = pydrive_retry(gdrive.GetAbout)
+ assert about_object is not None
+
+ # simulate token expiry by deleting the underlying token
+ gdrive.auth.credentials.token = None
+
+ # credential object should still exist but access token expired
+ assert gdrive.auth.credentials
+ assert gdrive.auth.access_token_expired
+
+ # as credentials have no refresh token, this would fail
+ with pytest.raises(RefreshError) as e_info:
+ about_object = pydrive_retry(gdrive.GetAbout)
Index: PyDrive2-1.16.2/setup.py
===================================================================
--- PyDrive2-1.16.2.orig/setup.py
+++ PyDrive2-1.16.2/setup.py
@@ -37,7 +37,8 @@ setup(
long_description_content_type="text/x-rst",
install_requires=[
"google-api-python-client >= 1.12.5",
- "oauth2client >= 4.0.0",
+ "google-auth",
+ "google-auth-oauthlib",
"PyYAML >= 3.0",
"pyOpenSSL >= 19.1.0",
],
Index: PyDrive2-1.16.2/pydrive2/test/test_oauth_custom.py
===================================================================
--- /dev/null
+++ PyDrive2-1.16.2/pydrive2/test/test_oauth_custom.py
@@ -0,0 +1,42 @@
+import pytest
+import os
+from pydrive2.auth import GoogleAuth
+from pydrive2.drive import GoogleDrive
+from pydrive2.test.test_util import (
+ settings_file_path,
+ setup_credentials,
+ delete_file,
+)
+
+
+@pytest.fixture
+def googleauth_preauth():
+ setup_credentials()
+ # Delete old credentials file
+ delete_file("credentials/default_user.dat")
+ ga = GoogleAuth(settings_file_path("default_user.yaml"))
+
+ return ga
+
+
+@pytest.mark.manual
+def test_01_CustomAuthWithSavingOfCredentials(googleauth_preauth):
+
+ credentials_file = googleauth_preauth.settings["save_credentials_file"]
+
+ assert not os.path.exists(credentials_file)
+
+ auth_url, state = googleauth_preauth.GetAuthUrl()
+ print("please visit this url: {}".format(auth_url))
+
+ googleauth_preauth.Authenticate(input("Please enter the auth code: "))
+
+ # credentials have been loaded
+ assert googleauth_preauth.credentials
+ # check that credentials file has been saved
+ assert os.path.exists(credentials_file)
+
+ gdrive = GoogleDrive(googleauth_preauth)
+
+ about_object = gdrive.GetAbout()
+ assert about_object is not None