Commit 1c8e3731 authored by Jérome Perrin's avatar Jérome Perrin

oauth_google_login: reimplement with oauthlib/requests

Because the librairies used here were never ported to python3.

Notable changes:

  - ERP5Site_createGoogleUserToOAuth is dropped
  - internal API changed radically, so customizations made by overriding
    scripts are broken.
  - the core logic is now implemented in a connector class (still in
    portal_oauth for simplicity, but it would be simpler to move it to
    portal_web_services)

No changes required in the google console, the redirect uri is still
ERP5Site_receiveGoogleCallback
parent 533359cc
##############################################################################
# Copyright (c) 2024 Nexedi SA and Contributors. All Rights Reserved.
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
##############################################################################
from AccessControl import ClassSecurityInfo
import random
import string
import time
import oauthlib.oauth2
import requests
from zExceptions import Unauthorized
from Products.ERP5Type.XMLObject import XMLObject
from Products.ERP5Type import Permissions
from Products.ERP5Type.Utils import unicode2str
from Products.ERP5Type.Timeout import getTimeLeft
AUTH_URL = 'https://accounts.google.com/o/oauth2/auth'
TOKEN_URL = 'https://accounts.google.com/o/oauth2/token'
USER_INFO_URL = 'https://www.googleapis.com/oauth2/v1/userinfo'
SCOPE_LIST = ['https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/userinfo.email']
# Default timeout (in seconds) for the HTTP request made to google servers to
# exchange the authorization code for a token.
DEFAULT_HTTP_TIMEOUT = 10
class GoogleConnector(XMLObject):
meta_type = 'ERP5 Google Connector'
portal_type = 'Google Connector'
security = ClassSecurityInfo()
security.declareObjectProtected(Permissions.AccessContentsInformation)
@security.public
def redirectToGoogleLoginPage(self, redirect_uri, RESPONSE):
"""Redirect to authorization page.
"""
authorization_url = self._getOAuthlibClient().prepare_request_uri(
uri=AUTH_URL,
redirect_uri=redirect_uri,
scope=SCOPE_LIST,
access_type="offline",
include_granted_scopes='true',
prompt="consent",
state=self._getAuthorizationState(),
)
return RESPONSE.redirect(authorization_url)
@security.public # XXX public but not publishable
def getTokenFromCode(self, state, code, redirect_uri):
self._verifyAuthorizationState(state)
body = self._getOAuthlibClient().prepare_request_body(
code=code,
client_secret=self.getSecretKey(),
redirect_uri=redirect_uri,
)
resp = requests.post(
TOKEN_URL,
data=body,
headers={'Content-Type': 'application/x-www-form-urlencoded'},
timeout=self._getTimeout(),
)
__traceback_info__ = (resp.content, resp.status_code)
resp.raise_for_status()
return self._getGoogleTokenFromJSONResponse(resp.json())
@security.private
def refreshToken(self, token):
"""Refresh auth token.
Used by Products.ERP5Security.ERP5ExternalOauth2ExtractionPlugin
"""
body = self._getOAuthlibClient().prepare_refresh_body(
client_id=self.getClientId(),
client_secret=self.getSecretKey(),
access_type="offline",
refresh_token=token['refresh_token'],
)
resp = requests.post(
TOKEN_URL,
data=body,
headers={'Content-Type': 'application/x-www-form-urlencoded'},
timeout=self._getTimeout(),
)
if not resp.ok:
return {}
return self._getGoogleTokenFromJSONResponse(resp.json())
@security.private
def getUserEntry(self, access_token):
resp = requests.get(
USER_INFO_URL,
headers={'Authorization': 'Bearer {}'.format(access_token)},
timeout=self._getTimeout(),
)
resp.raise_for_status()
google_entry = resp.json()
user_entry = {}
# remap user info
for erp5_key, google_key in (
('first_name', 'given_name'),
('last_name', 'family_name'),
('email', 'email'),
('reference', 'email'),
):
user_entry[erp5_key] = unicode2str(google_entry.get(google_key, ''))
return user_entry
def _getOAuthlibClient(self):
return oauthlib.oauth2.WebApplicationClient(
self.getClientId(),
access_type="offline",
)
def _getGoogleTokenFromJSONResponse(self, token):
return {
'access_token': unicode2str(token['access_token']),
'refresh_token': unicode2str(token['refresh_token']),
'expires_in': token['expires_in'],
'response_timestamp': time.time(),
'connector_relative_url': self.getRelativeUrl(),
}
def _getAuthorizationState(self):
alphabet = string.ascii_letters + string.digits
state = ''.join(random.SystemRandom().choice(alphabet) for _ in range(32))
self.getPortalObject().portal_sessions['google_login_auth_state'][state] = True
return state
def _verifyAuthorizationState(self, state):
if not self.getPortalObject().portal_sessions['google_login_auth_state'].pop(state, False):
raise Unauthorized
def _getTimeout(self):
"""Compute the time left according to publisher deadline.
"""
time_left = getTimeLeft()
if time_left is None:
time_left = DEFAULT_HTTP_TIMEOUT
return min(self.getTimeout() or DEFAULT_HTTP_TIMEOUT, time_left)
...@@ -2,13 +2,13 @@ ...@@ -2,13 +2,13 @@
<ZopeData> <ZopeData>
<record id="1" aka="AAAAAAAAAAE="> <record id="1" aka="AAAAAAAAAAE=">
<pickle> <pickle>
<global name="Extension Component" module="erp5.portal_type"/> <global name="Document Component" module="erp5.portal_type"/>
</pickle> </pickle>
<pickle> <pickle>
<dictionary> <dictionary>
<item> <item>
<key> <string>default_reference</string> </key> <key> <string>default_reference</string> </key>
<value> <string>GoogleLoginUtility</string> </value> <value> <string>GoogleConnector</string> </value>
</item> </item>
<item> <item>
<key> <string>description</string> </key> <key> <string>description</string> </key>
...@@ -18,11 +18,7 @@ ...@@ -18,11 +18,7 @@
</item> </item>
<item> <item>
<key> <string>id</string> </key> <key> <string>id</string> </key>
<value> <string>extension.erp5.GoogleLoginUtility</string> </value> <value> <string>document.erp5.GoogleConnector</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Extension Component</string> </value>
</item> </item>
<item> <item>
<key> <string>sid</string> </key> <key> <string>sid</string> </key>
......
import json
import oauth2client.client
import oauth2client.transport
from Products.ERP5Security.ERP5ExternalOauth2ExtractionPlugin import getGoogleUserEntry
from zExceptions import Unauthorized
SCOPE_LIST = ['https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/userinfo.email']
# Default timeout (in seconds) for the HTTP request made to google servers to
# exchange the authorization code for a token.
DEFAULT_HTTP_TIMEOUT = 10
def _getGoogleClientIdAndSecretKey(portal, reference="default"):
"""Returns google client id and secret key.
Internal function.
"""
result_list = unrestrictedSearchGoogleConnector(portal, reference=reference)
assert result_list, "Google Connector not found"
if len(result_list) == 2:
raise ValueError("Impossible to select one Google Connector")
google_connector = result_list[0].getObject()
return google_connector.getClientId(), google_connector.getSecretKey()
def redirectToGoogleLoginPage(self):
client_id, secret_key = _getGoogleClientIdAndSecretKey(self.getPortalObject())
flow = oauth2client.client.OAuth2WebServerFlow(
client_id=client_id,
client_secret=secret_key,
scope=SCOPE_LIST,
redirect_uri="{0}/ERP5Site_receiveGoogleCallback".format(self.absolute_url()),
access_type="offline",
prompt="consent",
include_granted_scopes="true")
self.REQUEST.RESPONSE.redirect(flow.step1_get_authorize_url())
def getAccessTokenFromCode(self, code, redirect_uri, timeout=DEFAULT_HTTP_TIMEOUT):
client_id, secret_key = _getGoogleClientIdAndSecretKey(self.getPortalObject())
flow = oauth2client.client.OAuth2WebServerFlow(
client_id=client_id,
client_secret=secret_key,
scope=SCOPE_LIST,
redirect_uri=redirect_uri,
access_type="offline",
include_granted_scopes="true")
credential = flow.step2_exchange(
code,
http=oauth2client.transport.get_http_object(timeout=timeout))
credential_data = json.loads(credential.to_json())
return credential_data
def unrestrictedSearchGoogleConnector(self, reference="default"):
return self.getPortalObject().portal_catalog.unrestrictedSearchResults(
portal_type="Google Connector",
reference=reference,
validation_state="validated",
limit=2)
def unrestrictedSearchGoogleLogin(self, login, REQUEST=None):
if REQUEST is not None:
raise Unauthorized
return self.getPortalObject().portal_catalog.unrestrictedSearchResults(
portal_type="Google Login",
reference=login,
validation_state="validated", limit=1)
def getUserEntry(access_token):
return getGoogleUserEntry(access_token)
\ No newline at end of file
<property_sheet_list> <property_sheet_list>
<portal_type id="Google Connector"> <portal_type id="Google Connector">
<item>OAuthClient</item> <item>OAuthClient</item>
<item>SocketClient</item>
</portal_type> </portal_type>
<portal_type id="Template Tool"> <portal_type id="Template Tool">
<item>TemplateToolERP5GoogleExtractionPluginConstraint</item> <item>TemplateToolERP5GoogleExtractionPluginConstraint</item>
......
...@@ -40,7 +40,7 @@ ...@@ -40,7 +40,7 @@
</item> </item>
<item> <item>
<key> <string>type_class</string> </key> <key> <string>type_class</string> </key>
<value> <string>XMLObject</string> </value> <value> <string>GoogleConnector</string> </value>
</item> </item>
<item> <item>
<key> <string>type_interface</string> </key> <key> <string>type_interface</string> </key>
......
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ExternalMethod" module="Products.ExternalMethod.ExternalMethod"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_function</string> </key>
<value> <string>getAccessTokenFromCode</string> </value>
</item>
<item>
<key> <string>_module</string> </key>
<value> <string>GoogleLoginUtility</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>ERP5Site_getAccessTokenFromCode</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
portal = context.getPortalObject()
result_list = portal.portal_catalog(
portal_type="Google Connector",
reference=reference,
validation_state="validated",
limit=2)
if len(result_list) != 1:
raise ValueError("Impossible to select one Google Connector")
return result_list[0].getObject()
...@@ -50,19 +50,19 @@ ...@@ -50,19 +50,19 @@
</item> </item>
<item> <item>
<key> <string>_params</string> </key> <key> <string>_params</string> </key>
<value> <string>login, REQUEST=None</string> </value> <value> <string>reference="default"</string> </value>
</item> </item>
<item> <item>
<key> <string>_proxy_roles</string> </key> <key> <string>_proxy_roles</string> </key>
<value> <value>
<tuple> <tuple>
<string>Manager</string> <string>Auditor</string>
</tuple> </tuple>
</value> </value>
</item> </item>
<item> <item>
<key> <string>id</string> </key> <key> <string>id</string> </key>
<value> <string>ERP5Site_getPersonFromGoogleLogin</string> </value> <value> <string>ERP5Site_getDefaultGoogleConnector</string> </value>
</item> </item>
</dictionary> </dictionary>
</pickle> </pickle>
......
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ExternalMethod" module="Products.ExternalMethod.ExternalMethod"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_function</string> </key>
<value> <string>unrestrictedSearchGoogleConnector</string> </value>
</item>
<item>
<key> <string>_module</string> </key>
<value> <string>GoogleLoginUtility</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>ERP5Site_getGoogleConnector</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ExternalMethod" module="Products.ExternalMethod.ExternalMethod"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_function</string> </key>
<value> <string>unrestrictedSearchGoogleConnector</string> </value>
</item>
<item>
<key> <string>_module</string> </key>
<value> <string>GoogleLoginUtility</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>ERP5Site_getGoogleLogin</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ExternalMethod" module="Products.ExternalMethod.ExternalMethod"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_function</string> </key>
<value> <string>getUserEntry</string> </value>
</item>
<item>
<key> <string>_module</string> </key>
<value> <string>GoogleLoginUtility</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>ERP5Site_getGoogleUserEntry</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
from zExceptions import Unauthorized
if REQUEST is not None:
raise Unauthorized
login = context.ERP5Site_getGoogleLogin(login)
if login is None:
return login
if len(login) > 1:
raise ValueError("Duplicated User")
return login[0].getParentValue().getRelativeUrl()
import time from Products.ERP5Type.Utils import str2bytes
portal = context.getPortalObject()
request = container.REQUEST request = container.REQUEST
response = request.RESPONSE response = request.RESPONSE
...@@ -16,27 +17,22 @@ if error is not None: ...@@ -16,27 +17,22 @@ if error is not None:
return handleError(error) return handleError(error)
elif code is not None: elif code is not None:
response_dict = context.ERP5Site_getAccessTokenFromCode( google_connector = portal.ERP5Site_getDefaultGoogleConnector()
code, response_dict = google_connector.getTokenFromCode(
"{0}/ERP5Site_receiveGoogleCallback".format(context.absolute_url())) state=state,
if response_dict is not None: code=code,
access_token = response_dict['access_token'].encode('utf-8') redirect_uri="{}/ERP5Site_receiveGoogleCallback".format(portal.absolute_url()),
hash_str = context.Base_getHMAC(access_token, access_token) )
context.setAuthCookie(response, '__ac_google_hash', hash_str)
# store timestamp in second since the epoch in UTC is enough access_token = str2bytes(response_dict['access_token'])
response_dict["response_timestamp"] = time.time() hash_str = portal.Base_getHMAC(access_token, access_token)
context.Base_setBearerToken(hash_str, portal.setAuthCookie(response, '__ac_google_hash', hash_str)
response_dict, portal.Base_setBearerToken(
"google_server_auth_token_cache_factory") hash_str,
user_dict = context.ERP5Site_getGoogleUserEntry(access_token) response_dict,
user_reference = user_dict["email"] "google_server_auth_token_cache_factory")
context.Base_setBearerToken(access_token,
{"reference": user_reference}, # XXX for ERP5JS web sites without a rewrite rule, we make sure there's a trailing /
"google_server_auth_token_cache_factory") return response.redirect(request.get("came_from") or context.absolute_url() + '/')
method = getattr(context, "ERP5Site_createGoogleUserToOAuth", None)
if method is not None:
method(user_reference, user_dict)
# XXX for ERP5JS web sites without a rewrite rule, we make sure there's a trailing /
return response.redirect(request.get("came_from") or context.absolute_url() + '/')
return handleError('') return handleError('')
...@@ -50,7 +50,7 @@ ...@@ -50,7 +50,7 @@
</item> </item>
<item> <item>
<key> <string>_params</string> </key> <key> <string>_params</string> </key>
<value> <string>code=None, error=None</string> </value> <value> <string>code=None, state=None, error=None</string> </value>
</item> </item>
<item> <item>
<key> <string>id</string> </key> <key> <string>id</string> </key>
......
portal = context.getPortalObject()
google_connector = portal.ERP5Site_getDefaultGoogleConnector()
return google_connector.redirectToGoogleLoginPage(
"{0}/ERP5Site_receiveGoogleCallback".format(portal.absolute_url()),
RESPONSE=RESPONSE,
)
...@@ -2,25 +2,67 @@ ...@@ -2,25 +2,67 @@
<ZopeData> <ZopeData>
<record id="1" aka="AAAAAAAAAAE="> <record id="1" aka="AAAAAAAAAAE=">
<pickle> <pickle>
<global name="ExternalMethod" module="Products.ExternalMethod.ExternalMethod"/> <global name="PythonScript" module="Products.PythonScripts.PythonScript"/>
</pickle> </pickle>
<pickle> <pickle>
<dictionary> <dictionary>
<item> <item>
<key> <string>_function</string> </key> <key> <string>_bind_names</string> </key>
<value> <string>redirectToGoogleLoginPage</string> </value> <value>
<object>
<klass>
<global name="_reconstructor" module="copy_reg"/>
</klass>
<tuple>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
<global name="object" module="__builtin__"/>
<none/>
</tuple>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary>
<item>
<key> <string>name_container</string> </key>
<value> <string>container</string> </value>
</item>
<item>
<key> <string>name_context</string> </key>
<value> <string>context</string> </value>
</item>
<item>
<key> <string>name_m_self</string> </key>
<value> <string>script</string> </value>
</item>
<item>
<key> <string>name_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item> </item>
<item> <item>
<key> <string>_module</string> </key> <key> <string>_params</string> </key>
<value> <string>GoogleLoginUtility</string> </value> <value> <string>RESPONSE</string> </value>
</item> </item>
<item> <item>
<key> <string>id</string> </key> <key> <string>_proxy_roles</string> </key>
<value> <string>ERP5Site_redirectToGoogleLoginPage</string> </value> <value>
<tuple>
<string>Auditor</string>
</tuple>
</value>
</item> </item>
<item> <item>
<key> <string>title</string> </key> <key> <string>id</string> </key>
<value> <string></string> </value> <value> <string>ERP5Site_redirectToGoogleLoginPage</string> </value>
</item> </item>
</dictionary> </dictionary>
</pickle> </pickle>
......
document.erp5.GoogleConnector
\ No newline at end of file
extension.erp5.GoogleLoginUtility
\ No newline at end of file
Google Connector | OAuthClient Google Connector | OAuthClient
Google Connector | SocketClient
Template Tool | TemplateToolERP5GoogleExtractionPluginConstraint Template Tool | TemplateToolERP5GoogleExtractionPluginConstraint
\ No newline at end of file
...@@ -38,10 +38,10 @@ from Products.ERP5Security import _setUserNameForAccessLog ...@@ -38,10 +38,10 @@ from Products.ERP5Security import _setUserNameForAccessLog
from AccessControl.SecurityManagement import getSecurityManager, \ from AccessControl.SecurityManagement import getSecurityManager, \
setSecurityManager, newSecurityManager setSecurityManager, newSecurityManager
from Products.ERP5Type.Cache import DEFAULT_CACHE_SCOPE from Products.ERP5Type.Cache import DEFAULT_CACHE_SCOPE
import time
import six import six
import time
from six.moves import urllib from six.moves import urllib
import json
from zLOG import LOG, ERROR, INFO from zLOG import LOG, ERROR, INFO
try: try:
...@@ -49,41 +49,13 @@ try: ...@@ -49,41 +49,13 @@ try:
except ImportError: except ImportError:
facebook = None facebook = None
try:
import apiclient.discovery
import httplib2
import oauth2client.client
except ImportError:
httplib2 = None
#Form for new plugin in ZMI # Form for new plugin in ZMI
manage_addERP5FacebookExtractionPluginForm = PageTemplateFile( manage_addERP5FacebookExtractionPluginForm = PageTemplateFile(
'www/ERP5Security_addERP5FacebookExtractionPlugin', globals(), 'www/ERP5Security_addERP5FacebookExtractionPlugin', globals(),
__name__='manage_addERP5FacebookExtractionPluginForm') __name__='manage_addERP5FacebookExtractionPluginForm')
def getGoogleUserEntry(token):
if httplib2 is None:
LOG('ERP5GoogleExtractionPlugin', INFO,
'No Google modules available, please install google-api-python-client '
'package. Authentication disabled..')
return None
http = oauth2client.client.AccessTokenCredentials(token,
'ERP5 Client'
).authorize(httplib2.Http(timeout=5))
service = apiclient.discovery.build("oauth2", "v1", http=http)
google_entry = service.userinfo().get().execute()
user_entry = {}
if google_entry is not None:
# sanitise value
for k in (('first_name', 'given_name'),
('last_name', 'family_name'),
('email', 'email'),
('reference', 'email'),):
value = google_entry.get(k[1], '').encode('utf-8')
user_entry[k[0]] = value
return user_entry
def addERP5FacebookExtractionPlugin(dispatcher, id, title=None, REQUEST=None): def addERP5FacebookExtractionPlugin(dispatcher, id, title=None, REQUEST=None):
""" Add a ERP5FacebookExtractionPlugin to a Pluggable Auth Service. """ """ Add a ERP5FacebookExtractionPlugin to a Pluggable Auth Service. """
...@@ -98,7 +70,7 @@ def addERP5FacebookExtractionPlugin(dispatcher, id, title=None, REQUEST=None): ...@@ -98,7 +70,7 @@ def addERP5FacebookExtractionPlugin(dispatcher, id, title=None, REQUEST=None):
'ERP5FacebookExtractionPlugin+added.' 'ERP5FacebookExtractionPlugin+added.'
% dispatcher.absolute_url()) % dispatcher.absolute_url())
#Form for new plugin in ZMI # Form for new plugin in ZMI
manage_addERP5GoogleExtractionPluginForm = PageTemplateFile( manage_addERP5GoogleExtractionPluginForm = PageTemplateFile(
'www/ERP5Security_addERP5GoogleExtractionPlugin', globals(), 'www/ERP5Security_addERP5GoogleExtractionPlugin', globals(),
__name__='manage_addERP5GoogleExtractionPluginForm') __name__='manage_addERP5GoogleExtractionPluginForm')
...@@ -157,13 +129,13 @@ class ERP5ExternalOauth2ExtractionPlugin: ...@@ -157,13 +129,13 @@ class ERP5ExternalOauth2ExtractionPlugin:
for cache_plugin in cache_factory.getCachePluginList(): for cache_plugin in cache_factory.getCachePluginList():
cache_entry = cache_plugin.get(key, DEFAULT_CACHE_SCOPE) cache_entry = cache_plugin.get(key, DEFAULT_CACHE_SCOPE)
if cache_entry is not None: if cache_entry is not None:
# Avoid errors if the plugin don't have the funcionality of refresh token
refreshTokenIfExpired = getattr(self, "refreshTokenIfExpired", None)
cache_value = cache_entry.getValue() cache_value = cache_entry.getValue()
if refreshTokenIfExpired is not None: # getToken is called for the access_token_dict and for
return refreshTokenIfExpired(key, cache_value) # the user entry. We try to refresh only for the
else: # access_token_dict
return cache_value if 'refresh_token' in cache_value:
return self.refreshTokenIfExpired(key, cache_value)
return cache_value
raise KeyError('Key %r not found' % key) raise KeyError('Key %r not found' % key)
#################################### ####################################
...@@ -171,19 +143,29 @@ class ERP5ExternalOauth2ExtractionPlugin: ...@@ -171,19 +143,29 @@ class ERP5ExternalOauth2ExtractionPlugin:
#################################### ####################################
security.declarePrivate('extractCredentials') security.declarePrivate('extractCredentials')
def extractCredentials(self, request): def extractCredentials(self, request):
""" Extract Oauth2 credentials from the request header. """ """ Extract Oauth2 credentials from cookie.
user_dict = {}
This plugins uses two level of cache storage:
- cookie_value => access_token_dict
- access_token => user_entry
access_token_dict depends on the concrete plugin classes,
but this is generally access_token and refresh_token.
user_entry must contain "reference", which is the reference
of the corresponding login document in ERP5.
"""
access_token_dict = {}
cookie_hash = request.get(self.cookie_name) cookie_hash = request.get(self.cookie_name)
if cookie_hash is not None: if cookie_hash is not None:
try: try:
user_dict = self.getToken(cookie_hash) access_token_dict = self.getToken(cookie_hash)
except KeyError: except KeyError:
LOG(self.getId(), INFO, 'Hash %s not found' % cookie_hash) LOG(self.getId(), INFO, 'Hash %s not found' % cookie_hash)
return {} return {}
token = None token = None
if "access_token" in user_dict: if "access_token" in access_token_dict:
token = user_dict["access_token"] token = access_token_dict["access_token"]
if token is None: if token is None:
# no token, then no credentials # no token, then no credentials
...@@ -193,7 +175,7 @@ class ERP5ExternalOauth2ExtractionPlugin: ...@@ -193,7 +175,7 @@ class ERP5ExternalOauth2ExtractionPlugin:
try: try:
user_entry = self.getToken(token) user_entry = self.getToken(token)
except KeyError: except KeyError:
user_entry = self.getUserEntry(token) user_entry = self.getUserEntry(access_token_dict)
if user_entry is not None: if user_entry is not None:
# Reduce data size because, we don't need more than reference # Reduce data size because, we don't need more than reference
user_entry = {"reference": user_entry["reference"]} user_entry = {"reference": user_entry["reference"]}
...@@ -263,40 +245,36 @@ class ERP5FacebookExtractionPlugin(ERP5ExternalOauth2ExtractionPlugin, BasePlugi ...@@ -263,40 +245,36 @@ class ERP5FacebookExtractionPlugin(ERP5ExternalOauth2ExtractionPlugin, BasePlugi
cookie_name = "__ac_facebook_hash" cookie_name = "__ac_facebook_hash"
cache_factory_name = "facebook_server_auth_token_cache_factory" cache_factory_name = "facebook_server_auth_token_cache_factory"
def refreshTokenIfExpired(self, key, cache_value): def refreshTokenIfExpired(self, key, access_token_dict):
return cache_value return access_token_dict
def getUserEntry(self, token): def getUserEntry(self, token):
return getFacebookUserDict(token) return getFacebookUserDict(token)
class ERP5GoogleExtractionPlugin(ERP5ExternalOauth2ExtractionPlugin, BasePlugin): class ERP5GoogleExtractionPlugin(ERP5ExternalOauth2ExtractionPlugin, BasePlugin):
""" """
Plugin to authenicate as machines. Plugin to authenticate using google OAuth2.
""" """
meta_type = "ERP5 Google Extraction Plugin" meta_type = "ERP5 Google Extraction Plugin"
login_portal_type = "Google Login" login_portal_type = "Google Login"
cookie_name = "__ac_google_hash" cookie_name = "__ac_google_hash"
cache_factory_name = "google_server_auth_token_cache_factory" cache_factory_name = "google_server_auth_token_cache_factory"
def refreshTokenIfExpired(self, key, cache_value): def refreshTokenIfExpired(self, key, access_token_dict):
expires_in = cache_value.get("token_response", {}).get("expires_in") if (time.time() - access_token_dict["response_timestamp"]) \
refresh_token = cache_value.get("refresh_token") >= access_token_dict['expires_in']:
if expires_in and refresh_token: access_token_dict = self.getPortalObject().unrestrictedTraverse(
if (time.time() - cache_value["response_timestamp"]) >= float(expires_in): access_token_dict['connector_relative_url']
credential = oauth2client.client.OAuth2Credentials( ).refreshToken(access_token_dict)
cache_value["access_token"], cache_value["client_id"], self.setToken(key, access_token_dict)
cache_value["client_secret"], refresh_token, return access_token_dict
cache_value["token_expiry"], cache_value["token_uri"],
cache_value["user_agent"]) def getUserEntry(self, access_token_dict):
credential.refresh(httplib2.Http(timeout=5)) return self.getPortalObject().unrestrictedTraverse(
cache_value = json.loads(credential.to_json()) access_token_dict['connector_relative_url'],
cache_value["response_timestamp"] = time.time() ).getUserEntry(access_token_dict['access_token'])
self.setToken(key, cache_value)
return cache_value
def getUserEntry(self, token):
return getGoogleUserEntry(token)
#List implementation of class #List implementation of class
classImplements( ERP5FacebookExtractionPlugin, classImplements( ERP5FacebookExtractionPlugin,
......
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