Repository: GoogleCloudPlatform/google-auth-library-python (python)

Issue:
AuthorizedSession attempts to refresh token when no refresh token was provided
Using google-auth 1.4.1

If `google.auth.transport.requests.AuthorizedSession` is used with an expired token, it will automatically try to refresh the token even if no refresh token was provided to the credentials object.
This causes the unreadable exception
```TransportError: Invalid URL 'None': No schema supplied. Perhaps you meant http://None?```

There should be a sanity check for a non-existing refresh token before any refresh attempt is made. A proper exception should be raised if the token is expired.

Sample code:
```python
import google.oauth2.credentials
from google.auth.transport.requests import AuthorizedSession
credentials = google.oauth2.credentials.Credentials('an_expired_token')
authed_session = AuthorizedSession(credentials)
response = authed_session.get('some_url_requiring_authentication')
```

Traceback:
```
  File "/usr/lib/python3.6/site-packages/requests/sessions.py", line 521, in get
    return self.request('GET', url, **kwargs)
  File "/usr/lib/python3.6/site-packages/google/auth/transport/requests.py", line 218, in request
    self.credentials.refresh(auth_request_with_timeout)
  File "/usr/lib/python3.6/site-packages/google/oauth2/credentials.py", line 126, in refresh
    self._client_secret))
  File "/usr/lib/python3.6/site-packages/google/oauth2/_client.py", line 237, in refresh_grant
    response_data = _token_endpoint_request(request, token_uri, body)
  File "/usr/lib/python3.6/site-packages/google/oauth2/_client.py", line 106, in _token_endpoint_request
    method='POST', url=token_uri, headers=headers, body=body)
  File "/usr/lib/python3.6/site-packages/google/auth/transport/requests.py", line 124, in __call__
    six.raise_from(new_exc, caught_exc)
  File "<string>", line 3, in raise_from
google.auth.exceptions.TransportError: Invalid URL 'None': No schema supplied. Perhaps you meant http://None?
```

Pull request:
Raise a helpful exception when trying to refresh credentials without a refresh token
Closes #258

Patch:
diff --git a/google/oauth2/credentials.py b/google/oauth2/credentials.py
index 24b3a3e..8e2a7f8 100644
--- a/google/oauth2/credentials.py
+++ b/google/oauth2/credentials.py
@@ -38,6 +38,7 @@ import six
 
 from google.auth import _helpers
 from google.auth import credentials
+from google.auth import exceptions
 from google.oauth2 import _client
 
 
@@ -120,6 +121,15 @@ class Credentials(credentials.ReadOnlyScoped, credentials.Credentials):
 
     @_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,


Test patch:
diff --git a/tests/oauth2/test_credentials.py b/tests/oauth2/test_credentials.py
index 9064363..922c3bb 100644
--- a/tests/oauth2/test_credentials.py
+++ b/tests/oauth2/test_credentials.py
@@ -17,8 +17,10 @@ import json
 import os
 
 import mock
+import pytest
 
 from google.auth import _helpers
+from google.auth import exceptions
 from google.auth import transport
 from google.oauth2 import credentials
 
@@ -95,6 +97,16 @@ class TestCredentials(object):
         # expired)
         assert credentials.valid
 
+    def test_refresh_no_refresh_token(self):
+        request = mock.create_autospec(transport.Request)
+        credentials_ = credentials.Credentials(
+            token=None, refresh_token=None)
+
+        with pytest.raises(exceptions.RefreshError, match='necessary fields'):
+            credentials_.refresh(request)
+
+        request.assert_not_called()
+
     def test_from_authorized_user_info(self):
         info = AUTH_USER_INFO.copy()