diff --git a/google/oauth2/credentials.py b/google/oauth2/credentials.py index 077a95f73..6a635ddad 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 @@ -79,6 +82,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.""" @@ -106,10 +120,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._id_token = grant_response.get('id_token') diff --git a/tests/oauth2/test_credentials.py b/tests/oauth2/test_credentials.py index b117ad4b7..e15766a89 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 = {'id_token': mock.sentinel.id_token} 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.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 774b977ee..e80b7d497 100644 --- a/tests/oauth2/test_service_account.py +++ b/tests/oauth2/test_service_account.py @@ -162,7 +162,9 @@ def test__make_authorization_grant_assertion_subject(self): def test_refresh_success(self, jwt_grant_mock): token = 'token' jwt_grant_mock.return_value = ( - token, _helpers.utcnow() + datetime.timedelta(seconds=500), None) + token, + _helpers.utcnow() + datetime.timedelta(seconds=500), + {}) request_mock = mock.Mock() # Refresh credentials