Add downscoping to ouath2 credentials (#309)
diff --git a/google/oauth2/_client.py b/google/oauth2/_client.py
index dc35be2..5121a32 100644
--- a/google/oauth2/_client.py
+++ b/google/oauth2/_client.py
@@ -201,7 +201,8 @@
return id_token, expiry, response_data
-def refresh_grant(request, token_uri, refresh_token, client_id, client_secret):
+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`_.
@@ -215,6 +216,10 @@
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
@@ -233,6 +238,8 @@
'client_secret': client_secret,
'refresh_token': refresh_token,
}
+ if scopes:
+ body['scope'] = ' '.join(scopes)
response_data = _token_endpoint_request(request, token_uri, body)
diff --git a/google/oauth2/credentials.py b/google/oauth2/credentials.py
index 4cb909c..b56e314 100644
--- a/google/oauth2/credentials.py
+++ b/google/oauth2/credentials.py
@@ -67,10 +67,13 @@
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 that were originally used
- to obtain authorization. This is a purely informative parameter
- that can be used by :meth:`has_scopes`. OAuth 2.0 credentials
- can not request additional scopes after authorization.
+ 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').
"""
super(Credentials, self).__init__()
self.token = token
@@ -133,13 +136,24 @@
access_token, refresh_token, expiry, grant_response = (
_client.refresh_grant(
request, self._token_uri, self._refresh_token, self._client_id,
- self._client_secret))
+ 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.