From 8f7066d94b8d93af9213bfcebac184df1add8c0c Mon Sep 17 00:00:00 2001 From: Jon Wayne Parrott Date: Mon, 27 Mar 2017 13:45:40 -0700 Subject: [PATCH 1/4] Expose the refresh grant response in OAuth 2.0 credentials --- google/oauth2/credentials.py | 20 +++++++++++++++++--- google/oauth2/service_account.py | 16 +++++++++++++++- tests/oauth2/test_credentials.py | 4 +++- tests/oauth2/test_service_account.py | 6 +++++- 4 files changed, 40 insertions(+), 6 deletions(-) diff --git a/google/oauth2/credentials.py b/google/oauth2/credentials.py index 077a95f73..9f254324e 100644 --- a/google/oauth2/credentials.py +++ b/google/oauth2/credentials.py @@ -67,6 +67,7 @@ def __init__(self, token, refresh_token=None, token_uri=None, self._token_uri = token_uri self._client_id = client_id self._client_secret = client_secret + self._refresh_grant_response = None @property def refresh_token(self): @@ -89,6 +90,17 @@ def client_secret(self): """Optional[str]: The OAuth 2.0 client secret.""" return self._client_secret + @property + def refresh_grant_response(self): + """Optional[str]: The last response from the OAuth 2.0 token server. + + This is set when :meth:`refresh` is called and will not be populated + otherwise. This is provided because some authorization servers will + send along additional information other than the access and refresh + tokens in the refresh grant response. + """ + return self._refresh_grant_response + @property def requires_scopes(self): """False: OAuth 2.0 credentials have their scopes set when @@ -106,10 +118,12 @@ def with_scopes(self, scopes): @_helpers.copy_docstring(credentials.Credentials) def refresh(self, request): - access_token, refresh_token, expiry, _ = _client.refresh_grant( - request, self._token_uri, self._refresh_token, self._client_id, - self._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.token = access_token self.expiry = expiry self._refresh_token = refresh_token + self._refresh_grant_response = grant_response diff --git a/google/oauth2/service_account.py b/google/oauth2/service_account.py index f8a27bfa7..9b09a9158 100644 --- a/google/oauth2/service_account.py +++ b/google/oauth2/service_account.py @@ -148,6 +148,8 @@ def __init__(self, signer, service_account_email, token_uri, scopes=None, else: self._additional_claims = {} + self._refresh_grant_response = None + @classmethod def _from_signer_and_info(cls, signer, info, **kwargs): """Creates a Credentials instance from a signer and service account @@ -205,6 +207,17 @@ def from_service_account_file(cls, filename, **kwargs): filename, require=['client_email', 'token_uri']) return cls._from_signer_and_info(signer, info, **kwargs) + @property + def refresh_grant_response(self): + """Optional[str]: The last response from the OAuth 2.0 token server. + + This is set when :meth:`refresh` is called and will not be populated + otherwise. This is provided because some authorization servers will + send along additional information other than the access and refresh + tokens in the refresh grant response. + """ + return self._refresh_grant_response + @property def service_account_email(self): """The service account email.""" @@ -306,10 +319,11 @@ def _make_authorization_grant_assertion(self): @_helpers.copy_docstring(credentials.Credentials) def refresh(self, request): assertion = self._make_authorization_grant_assertion() - access_token, expiry, _ = _client.jwt_grant( + access_token, expiry, refresh_grant_response = _client.jwt_grant( request, self._token_uri, assertion) self.token = access_token self.expiry = expiry + self._refresh_grant_response = refresh_grant_response @_helpers.copy_docstring(credentials.Signing) def sign_bytes(self, message): diff --git a/tests/oauth2/test_credentials.py b/tests/oauth2/test_credentials.py index b117ad4b7..e8726a4af 100644 --- a/tests/oauth2/test_credentials.py +++ b/tests/oauth2/test_credentials.py @@ -58,6 +58,7 @@ def test_create_scoped(self): def test_refresh_success(self, now_mock, refresh_grant_mock): token = 'token' expiry = _helpers.utcnow() + datetime.timedelta(seconds=500) + grant_response = {'meep': 'moop'} refresh_grant_mock.return_value = ( # Access token token, @@ -66,7 +67,7 @@ def test_refresh_success(self, now_mock, refresh_grant_mock): # Expiry, expiry, # Extra data - {}) + grant_response) request_mock = mock.Mock() # Refresh credentials @@ -80,6 +81,7 @@ def test_refresh_success(self, now_mock, refresh_grant_mock): # Check that the credentials have the token and expiry assert self.credentials.token == token assert self.credentials.expiry == expiry + assert self.credentials.refresh_grant_response == grant_response # Check that the credentials are valid (have a token and are not # expired) diff --git a/tests/oauth2/test_service_account.py b/tests/oauth2/test_service_account.py index 774b977ee..824be3223 100644 --- a/tests/oauth2/test_service_account.py +++ b/tests/oauth2/test_service_account.py @@ -161,8 +161,11 @@ def test__make_authorization_grant_assertion_subject(self): @mock.patch('google.oauth2._client.jwt_grant', autospec=True) def test_refresh_success(self, jwt_grant_mock): token = 'token' + grant_response = {'meep': 'moop'} jwt_grant_mock.return_value = ( - token, _helpers.utcnow() + datetime.timedelta(seconds=500), None) + token, + _helpers.utcnow() + datetime.timedelta(seconds=500), + grant_response) request_mock = mock.Mock() # Refresh credentials @@ -179,6 +182,7 @@ def test_refresh_success(self, jwt_grant_mock): # Check that the credentials have the token. assert self.credentials.token == token + assert self.credentials.refresh_grant_response == grant_response # Check that the credentials are valid (have a token and are not # expired) From 0a5d08272144a9938beecc09b8b519602814697a Mon Sep 17 00:00:00 2001 From: Jon Wayne Parrott Date: Tue, 28 Mar 2017 12:42:58 -0700 Subject: [PATCH 2/4] Expose only the id token --- google/oauth2/credentials.py | 31 +++++++++++++++------------- google/oauth2/service_account.py | 16 +------------- tests/oauth2/test_credentials.py | 4 ++-- tests/oauth2/test_service_account.py | 4 +--- 4 files changed, 21 insertions(+), 34 deletions(-) diff --git a/google/oauth2/credentials.py b/google/oauth2/credentials.py index 9f254324e..27c3447d2 100644 --- a/google/oauth2/credentials.py +++ b/google/oauth2/credentials.py @@ -39,14 +39,16 @@ class Credentials(credentials.Scoped, credentials.Credentials): """Credentials using OAuth 2.0 access and refresh tokens.""" - def __init__(self, token, refresh_token=None, token_uri=None, - client_id=None, client_secret=None, scopes=None): + def __init__(self, token, refresh_token=None, id_token=None, + token_uri=None, client_id=None, client_secret=None, + scopes=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. @@ -63,6 +65,7 @@ def __init__(self, token, refresh_token=None, token_uri=None, super(Credentials, self).__init__() self.token = token self._refresh_token = refresh_token + self._id_token = id_token self._scopes = scopes self._token_uri = token_uri self._client_id = client_id @@ -80,6 +83,17 @@ def token_uri(self): 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.""" @@ -90,17 +104,6 @@ def client_secret(self): """Optional[str]: The OAuth 2.0 client secret.""" return self._client_secret - @property - def refresh_grant_response(self): - """Optional[str]: The last response from the OAuth 2.0 token server. - - This is set when :meth:`refresh` is called and will not be populated - otherwise. This is provided because some authorization servers will - send along additional information other than the access and refresh - tokens in the refresh grant response. - """ - return self._refresh_grant_response - @property def requires_scopes(self): """False: OAuth 2.0 credentials have their scopes set when @@ -126,4 +129,4 @@ def refresh(self, request): self.token = access_token self.expiry = expiry self._refresh_token = refresh_token - self._refresh_grant_response = grant_response + self._id_token = grant_response.get('id_token', self._id_token) diff --git a/google/oauth2/service_account.py b/google/oauth2/service_account.py index 9b09a9158..f8a27bfa7 100644 --- a/google/oauth2/service_account.py +++ b/google/oauth2/service_account.py @@ -148,8 +148,6 @@ def __init__(self, signer, service_account_email, token_uri, scopes=None, else: self._additional_claims = {} - self._refresh_grant_response = None - @classmethod def _from_signer_and_info(cls, signer, info, **kwargs): """Creates a Credentials instance from a signer and service account @@ -207,17 +205,6 @@ def from_service_account_file(cls, filename, **kwargs): filename, require=['client_email', 'token_uri']) return cls._from_signer_and_info(signer, info, **kwargs) - @property - def refresh_grant_response(self): - """Optional[str]: The last response from the OAuth 2.0 token server. - - This is set when :meth:`refresh` is called and will not be populated - otherwise. This is provided because some authorization servers will - send along additional information other than the access and refresh - tokens in the refresh grant response. - """ - return self._refresh_grant_response - @property def service_account_email(self): """The service account email.""" @@ -319,11 +306,10 @@ def _make_authorization_grant_assertion(self): @_helpers.copy_docstring(credentials.Credentials) def refresh(self, request): assertion = self._make_authorization_grant_assertion() - access_token, expiry, refresh_grant_response = _client.jwt_grant( + access_token, expiry, _ = _client.jwt_grant( request, self._token_uri, assertion) self.token = access_token self.expiry = expiry - self._refresh_grant_response = refresh_grant_response @_helpers.copy_docstring(credentials.Signing) def sign_bytes(self, message): diff --git a/tests/oauth2/test_credentials.py b/tests/oauth2/test_credentials.py index e8726a4af..e15766a89 100644 --- a/tests/oauth2/test_credentials.py +++ b/tests/oauth2/test_credentials.py @@ -58,7 +58,7 @@ def test_create_scoped(self): def test_refresh_success(self, now_mock, refresh_grant_mock): token = 'token' expiry = _helpers.utcnow() + datetime.timedelta(seconds=500) - grant_response = {'meep': 'moop'} + grant_response = {'id_token': mock.sentinel.id_token} refresh_grant_mock.return_value = ( # Access token token, @@ -81,7 +81,7 @@ def test_refresh_success(self, now_mock, refresh_grant_mock): # Check that the credentials have the token and expiry assert self.credentials.token == token assert self.credentials.expiry == expiry - assert self.credentials.refresh_grant_response == grant_response + assert self.credentials.id_token == mock.sentinel.id_token # Check that the credentials are valid (have a token and are not # expired) diff --git a/tests/oauth2/test_service_account.py b/tests/oauth2/test_service_account.py index 824be3223..e80b7d497 100644 --- a/tests/oauth2/test_service_account.py +++ b/tests/oauth2/test_service_account.py @@ -161,11 +161,10 @@ def test__make_authorization_grant_assertion_subject(self): @mock.patch('google.oauth2._client.jwt_grant', autospec=True) def test_refresh_success(self, jwt_grant_mock): token = 'token' - grant_response = {'meep': 'moop'} jwt_grant_mock.return_value = ( token, _helpers.utcnow() + datetime.timedelta(seconds=500), - grant_response) + {}) request_mock = mock.Mock() # Refresh credentials @@ -182,7 +181,6 @@ def test_refresh_success(self, jwt_grant_mock): # Check that the credentials have the token. assert self.credentials.token == token - assert self.credentials.refresh_grant_response == grant_response # Check that the credentials are valid (have a token and are not # expired) From 7c0cc4aebfa8507b1ac9e07b32d9e581cf5486e9 Mon Sep 17 00:00:00 2001 From: Jon Wayne Parrott Date: Tue, 28 Mar 2017 12:48:44 -0700 Subject: [PATCH 3/4] Remove lingering property --- google/oauth2/credentials.py | 1 - 1 file changed, 1 deletion(-) diff --git a/google/oauth2/credentials.py b/google/oauth2/credentials.py index 27c3447d2..404af8878 100644 --- a/google/oauth2/credentials.py +++ b/google/oauth2/credentials.py @@ -70,7 +70,6 @@ def __init__(self, token, refresh_token=None, id_token=None, self._token_uri = token_uri self._client_id = client_id self._client_secret = client_secret - self._refresh_grant_response = None @property def refresh_token(self): From 0c82e866050e70ec9ae288a6f13b4a2e3dd12f74 Mon Sep 17 00:00:00 2001 From: Jon Wayne Parrott Date: Tue, 28 Mar 2017 12:56:29 -0700 Subject: [PATCH 4/4] Make id_token match the last response from the authorization server --- google/oauth2/credentials.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/google/oauth2/credentials.py b/google/oauth2/credentials.py index 404af8878..6a635ddad 100644 --- a/google/oauth2/credentials.py +++ b/google/oauth2/credentials.py @@ -128,4 +128,4 @@ def refresh(self, request): self.token = access_token self.expiry = expiry self._refresh_token = refresh_token - self._id_token = grant_response.get('id_token', self._id_token) + self._id_token = grant_response.get('id_token')