Commit 40d752f4 authored by Jérome Perrin's avatar Jérome Perrin

oauth_google_login: support refresh when refresh_token is still valid

When refresh_token is still valid google's endpoint does not include the
current refresh token in the response when refreshing the token, we need
to keep the current one.
This fixes user logout every one hour.
parent 130047f0
...@@ -96,11 +96,12 @@ class GoogleConnector(XMLObject): ...@@ -96,11 +96,12 @@ class GoogleConnector(XMLObject):
Used by Products.ERP5Security.ERP5ExternalOauth2ExtractionPlugin Used by Products.ERP5Security.ERP5ExternalOauth2ExtractionPlugin
""" """
refresh_token = token['refresh_token']
body = self._getOAuthlibClient().prepare_refresh_body( body = self._getOAuthlibClient().prepare_refresh_body(
client_id=self.getClientId(), client_id=self.getClientId(),
client_secret=self.getSecretKey(), client_secret=self.getSecretKey(),
access_type="offline", access_type="offline",
refresh_token=token['refresh_token'], refresh_token=refresh_token,
) )
resp = requests.post( resp = requests.post(
TOKEN_URL, TOKEN_URL,
...@@ -110,7 +111,9 @@ class GoogleConnector(XMLObject): ...@@ -110,7 +111,9 @@ class GoogleConnector(XMLObject):
) )
if not resp.ok: if not resp.ok:
return {} return {}
return self._getGoogleTokenFromJSONResponse(resp.json()) new_token = resp.json()
new_token.setdefault('refresh_token', refresh_token)
return self._getGoogleTokenFromJSONResponse(new_token)
@security.private @security.private
def getUserEntry(self, access_token): def getUserEntry(self, access_token):
......
...@@ -310,7 +310,7 @@ class TestGoogleLogin(GoogleLoginTestCase): ...@@ -310,7 +310,7 @@ class TestGoogleLogin(GoogleLoginTestCase):
resp = self.publish(self.portal.getPath(), env=env) resp = self.publish(self.portal.getPath(), env=env)
self.assertEqual(resp.getStatus(), six.moves.http_client.OK) self.assertEqual(resp.getStatus(), six.moves.http_client.OK)
def token_callback(request): def _check_token_callback_request(request, refresh_token):
self.assertEqual( self.assertEqual(
request.headers['Content-Type'], request.headers['Content-Type'],
'application/x-www-form-urlencoded') 'application/x-www-form-urlencoded')
...@@ -321,36 +321,81 @@ class TestGoogleLogin(GoogleLoginTestCase): ...@@ -321,36 +321,81 @@ class TestGoogleLogin(GoogleLoginTestCase):
'client_id': self.client_id, 'client_id': self.client_id,
'client_secret': self.secret_key, 'client_secret': self.secret_key,
'grant_type': 'refresh_token', 'grant_type': 'refresh_token',
'refresh_token': self.refresh_token, 'refresh_token': refresh_token,
} }
) )
def _userinfo_callback(request, access_token):
self.assertEqual(
request.headers['Authorization'],
'Bearer ' + access_token)
return 200, {}, json.dumps({
"first_name": "John",
"last_name": "Doe",
"email": self.default_google_login_email_address,
})
def token_callback_1(request):
# First refresh, the refresh token is still valid, so it is not
# included in the response, the client gets a new access_token
# and will re-use the same refresh_token
_check_token_callback_request(request, self.refresh_token)
return 200, {}, json.dumps({ return 200, {}, json.dumps({
'access_token': 'new' + self.access_token, 'access_token': 'new' + self.access_token,
'expires_in': 3600,
})
def userinfo_callback_1(request):
return _userinfo_callback(request, 'new' + self.access_token)
def token_callback_2(request):
# Second refresh, the refresh token is no longer valid and a new
# token is included in the response, the client gets a new access_token
# and and a new refresh_token
_check_token_callback_request(request, self.refresh_token)
return 200, {}, json.dumps({
'access_token': 'newnew' + self.access_token,
'refresh_token': 'new' + self.refresh_token, 'refresh_token': 'new' + self.refresh_token,
'expires_in': 3600, 'expires_in': 3600,
}) })
with mock.patch( def userinfo_callback_2(request):
'Products.ERP5Security.ERP5ExternalOauth2ExtractionPlugin.time.time', return _userinfo_callback(request, 'newnew' + self.access_token)
return_value=time.time() + 5000), \
responses.RequestsMock() as rsps: def token_callback_3(request):
rsps.add_callback( # Third refresh, we check that the refresh is made with the new refresh
responses.POST, # token from second refresh.
'https://accounts.google.com/o/oauth2/token', _check_token_callback_request(request, 'new' + self.refresh_token)
token_callback, return 200, {}, json.dumps({
) 'access_token': 'newnewnew' + self.access_token,
# refreshing the token calls userinfo again 'expires_in': 3600,
rsps.add( })
method='GET',
url='https://www.googleapis.com/oauth2/v1/userinfo', def userinfo_callback_3(request):
json={ return _userinfo_callback(request, 'newnewnew' + self.access_token)
"first_name": "John",
"last_name": "Doe", for i, (token_callback, userinfo_callback) in enumerate(
"email": self.default_google_login_email_address, zip(
} (token_callback_1, token_callback_2, token_callback_3),
) (userinfo_callback_1, userinfo_callback_2, userinfo_callback_3),
resp = self.publish(self.portal.getPath(), env=env) ), 1):
self.assertEqual(resp.getStatus(), six.moves.http_client.OK) with mock.patch(
'Products.ERP5Security.ERP5ExternalOauth2ExtractionPlugin.time.time',
return_value=time.time() + i * 5000), \
responses.RequestsMock() as rsps:
rsps.add_callback(
responses.POST,
'https://accounts.google.com/o/oauth2/token',
token_callback,
)
# refreshing the token calls userinfo again, with the new access token
rsps.add_callback(
responses.GET,
'https://www.googleapis.com/oauth2/v1/userinfo',
userinfo_callback,
)
resp = self.publish(self.portal.getPath(), env=env)
self.assertEqual(resp.getStatus(), six.moves.http_client.OK)
resp = self.publish(self.portal.getPath(), env=env) resp = self.publish(self.portal.getPath(), env=env)
self.assertEqual(resp.getStatus(), six.moves.http_client.OK) self.assertEqual(resp.getStatus(), six.moves.http_client.OK)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment