Commit fc12440f authored by Gabriel Monnerat's avatar Gabriel Monnerat Committed by Jérome Perrin

erp5_oauth_google_login: Set access_type as offline to be possible refresh...

erp5_oauth_google_login: Set access_type as offline to be possible refresh token in background and automatically

access_type as offline indicates whether your application can refresh access tokens when the user is not present at the browser.

This value instructs the Google authorization server to return a refresh token and an access token the first time that your application exchanges an authorization code for tokens state.

Also the code was simplied to use oauth2client rather than http requests directly use persistent cache instead of ram cache to lose token if we restart all nodes
parent 8d1dcc93
import httplib
import urllib
import json import json
import httplib2 import httplib2
import apiclient.discovery import apiclient.discovery
...@@ -7,36 +5,33 @@ import oauth2client.client ...@@ -7,36 +5,33 @@ import oauth2client.client
import socket import socket
from zLOG import LOG, ERROR from zLOG import LOG, ERROR
def getAccessTokenFromCode(self, code, redirect_uri): SCOPE_LIST = ['https://www.googleapis.com/auth/userinfo.profile',
connection_kw = {'host': 'accounts.google.com', 'timeout': 30} 'https://www.googleapis.com/auth/userinfo.email']
connection = httplib.HTTPSConnection(**connection_kw)
data = {
'client_id': self.portal_preferences.getPreferredGoogleClientId(),
'client_secret': self.portal_preferences.getPreferredGoogleSecretKey(),
'grant_type': 'authorization_code',
'redirect_uri': redirect_uri,
'code': code
}
data = urllib.urlencode(data)
headers = {
"Content-Type": "application/x-www-form-urlencoded",
"Accept": "*/*"
}
connection.request('POST', '/o/oauth2/token', data, headers)
response = connection.getresponse()
status = response.status
if status != 200:
return status, None
try: def redirectToGoogleLoginPage(self):
body = json.loads(response.read()) portal = self.getPortalObject()
except Exception, error_str: flow = oauth2client.client.OAuth2WebServerFlow(
return status, {"error": error_str} client_id=portal.portal_preferences.getPreferredGoogleClientId(),
client_secret=portal.portal_preferences.getPreferredGoogleSecretKey(),
scope=SCOPE_LIST,
redirect_uri="{0}/ERP5Site_receiveGoogleCallback".format(portal.absolute_url()),
access_type="offline",
prompt="consent",
include_granted_scopes="true")
self.REQUEST.RESPONSE.redirect(flow.step1_get_authorize_url())
try: def getAccessTokenFromCode(self, code, redirect_uri):
return status, body portal = self.getPortalObject()
except Exception: flow = oauth2client.client.OAuth2WebServerFlow(
return status, None client_id=portal.portal_preferences.getPreferredGoogleClientId(),
client_secret=portal.portal_preferences.getPreferredGoogleSecretKey(),
scope=SCOPE_LIST,
redirect_uri=redirect_uri,
access_type="offline",
include_granted_scopes="true")
credential = flow.step2_exchange(code)
credential_data = json.loads(credential.to_json())
return credential_data
def getUserId(access_token): def getUserId(access_token):
timeout = socket.getdefaulttimeout() timeout = socket.getdefaulttimeout()
......
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
</item> </item>
<item> <item>
<key> <string>cache_duration</string> </key> <key> <string>cache_duration</string> </key>
<value> <int>3600</int> </value> <value> <int>86400</int> </value>
</item> </item>
<item> <item>
<key> <string>description</string> </key> <key> <string>description</string> </key>
......
...@@ -2,17 +2,33 @@ ...@@ -2,17 +2,33 @@
<ZopeData> <ZopeData>
<record id="1" aka="AAAAAAAAAAE="> <record id="1" aka="AAAAAAAAAAE=">
<pickle> <pickle>
<global name="Ram Cache" module="erp5.portal_type"/> <global name="Distributed Ram Cache" module="erp5.portal_type"/>
</pickle> </pickle>
<pickle> <pickle>
<dictionary> <dictionary>
<item>
<key> <string>categories</string> </key>
<value>
<tuple>
<string>specialise/portal_memcached/persistent_memcached_plugin</string>
</tuple>
</value>
</item>
<item> <item>
<key> <string>id</string> </key> <key> <string>id</string> </key>
<value> <string>volatile_cache_plugin</string> </value> <value> <string>persistent_cache_plugin</string> </value>
</item>
<item>
<key> <string>int_index</string> </key>
<value> <int>1</int> </value>
</item> </item>
<item> <item>
<key> <string>portal_type</string> </key> <key> <string>portal_type</string> </key>
<value> <string>Ram Cache</string> </value> <value> <string>Distributed Ram Cache</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>persistent_cache</string> </value>
</item> </item>
</dictionary> </dictionary>
</pickle> </pickle>
......
import time
def handleError(error): def handleError(error):
context.Base_redirect( context.Base_redirect(
'login_form', 'login_form',
...@@ -9,22 +11,24 @@ def handleError(error): ...@@ -9,22 +11,24 @@ def handleError(error):
if error is not None: if error is not None:
return handleError(error) return handleError(error)
elif code is not None: elif code is not None:
portal = context.getPortalObject() portal = context.getPortalObject()
status, response_dict = context.ERP5Site_getAccessTokenFromCode( response_dict = context.ERP5Site_getAccessTokenFromCode(
code, code,
"{0}/ERP5Site_receiveGoogleCallback".format(portal.absolute_url())) "{0}/ERP5Site_receiveGoogleCallback".format(portal.absolute_url()))
if status != 200 and response_dict is not None:
return handleError(
" ".join(["%s : %s" % (k,v) for k,v in response_dict.iteritems()]))
if response_dict is not None: if response_dict is not None:
access_token = response_dict['access_token'].encode('utf-8') access_token = response_dict['access_token'].encode('utf-8')
response_dict['login'] = context.ERP5Site_getGoogleUserId(access_token)
hash_str = context.Base_getHMAC(access_token, access_token) hash_str = context.Base_getHMAC(access_token, access_token)
context.REQUEST.RESPONSE.setCookie('__ac_google_hash', hash_str, path='/') context.REQUEST.RESPONSE.setCookie('__ac_google_hash', hash_str, path='/')
# store timestamp in second since the epoch in UTC is enough
response_dict["response_timestamp"] = time.time()
context.Base_setBearerToken(hash_str, context.Base_setBearerToken(hash_str,
response_dict, response_dict,
"google_server_auth_token_cache_factory") "google_server_auth_token_cache_factory")
context.Base_setBearerToken(access_token,
{"reference": context.ERP5Site_getGoogleUserId(access_token)},
"google_server_auth_token_cache_factory")
return context.REQUEST.RESPONSE.redirect( return context.REQUEST.RESPONSE.redirect(
context.REQUEST.get("came_from") or portal.absolute_url()) context.REQUEST.get("came_from") or portal.absolute_url())
......
from ZTUtils import make_query
portal = context.getPortalObject()
query = make_query({
'response_type': 'code',
'client_id': portal.portal_preferences.getPreferredGoogleClientId(),
'redirect_uri': "{0}/ERP5Site_receiveGoogleCallback".format(portal.absolute_url()),
'scope': 'https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email'
})
context.REQUEST.RESPONSE.redirect("https://accounts.google.com/o/oauth2/auth?" + query)
...@@ -2,60 +2,26 @@ ...@@ -2,60 +2,26 @@
<ZopeData> <ZopeData>
<record id="1" aka="AAAAAAAAAAE="> <record id="1" aka="AAAAAAAAAAE=">
<pickle> <pickle>
<global name="PythonScript" module="Products.PythonScripts.PythonScript"/> <global name="ExternalMethod" module="Products.ExternalMethod.ExternalMethod"/>
</pickle> </pickle>
<pickle> <pickle>
<dictionary> <dictionary>
<item> <item>
<key> <string>Script_magic</string> </key> <key> <string>_function</string> </key>
<value> <int>3</int> </value> <value> <string>redirectToGoogleLoginPage</string> </value>
</item> </item>
<item> <item>
<key> <string>_bind_names</string> </key> <key> <string>_module</string> </key>
<value> <value> <string>GoogleLoginUtility</string> </value>
<object>
<klass>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
</klass>
<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>
<item> <item>
<key> <string>name_context</string> </key> <key> <string>id</string> </key>
<value> <string>context</string> </value> <value> <string>ERP5Site_redirectToGoogleLoginPage</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>_params</string> </key> <key> <string>title</string> </key>
<value> <string></string> </value> <value> <string></string> </value>
</item> </item>
<item>
<key> <string>id</string> </key>
<value> <string>ERP5Site_redirectToGoogleLoginPage</string> </value>
</item>
</dictionary> </dictionary>
</pickle> </pickle>
</record> </record>
......
...@@ -34,11 +34,14 @@ from Products.PluggableAuthService.interfaces import plugins ...@@ -34,11 +34,14 @@ from Products.PluggableAuthService.interfaces import plugins
from Products.PluggableAuthService.utils import classImplements from Products.PluggableAuthService.utils import classImplements
from Products.PluggableAuthService.plugins.BasePlugin import BasePlugin from Products.PluggableAuthService.plugins.BasePlugin import BasePlugin
from Products import ERP5Security from Products import ERP5Security
from Products.PluggableAuthService.PluggableAuthService import DumbHTTPExtractor
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 socket import socket
import httplib
import urllib
import json
from zLOG import LOG, ERROR, INFO from zLOG import LOG, ERROR, INFO
try: try:
...@@ -115,7 +118,7 @@ class ERP5ExternalOauth2ExtractionPlugin: ...@@ -115,7 +118,7 @@ class ERP5ExternalOauth2ExtractionPlugin:
cache_tool.updateCache() cache_tool.updateCache()
cache_factory = cache_tool.getRamCacheRoot().get(self.cache_factory_name) cache_factory = cache_tool.getRamCacheRoot().get(self.cache_factory_name)
if cache_factory is None: if cache_factory is None:
raise KeyError raise KeyError("Cache Factory %s not found" % self.cache_factory)
return cache_factory return cache_factory
def setToken(self, key, body): def setToken(self, key, body):
...@@ -130,7 +133,7 @@ class ERP5ExternalOauth2ExtractionPlugin: ...@@ -130,7 +133,7 @@ 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:
return cache_entry.getValue() return self.refreshTokenIfExpired(key, cache_entry.getValue())
raise KeyError('Key %r not found' % key) raise KeyError('Key %r not found' % key)
#################################### ####################################
...@@ -145,41 +148,40 @@ class ERP5ExternalOauth2ExtractionPlugin: ...@@ -145,41 +148,40 @@ class ERP5ExternalOauth2ExtractionPlugin:
user_dict = self.getToken(cookie_hash) user_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 DumbHTTPExtractor().extractCredentials(request) return {}
token = None token = None
if "access_token" in user_dict: if "access_token" in user_dict:
token = user_dict["access_token"] token = user_dict["access_token"]
if token is None: if token is None:
# no token # no token, then no credentials
return DumbHTTPExtractor().extractCredentials(request) return {}
# token is available
user = None
user_entry = None user_entry = None
try: try:
user = self.getToken(self.prefix + token) user_entry = self.getToken(token)
except KeyError: except KeyError:
user_entry = self.getUserEntry(token) user_entry = self.getUserEntry(token)
if user_entry is not None: if user_entry is not None:
user = user_entry["reference"] # Reduce data size because, we don't need more than reference
user_entry = {"reference": user_entry["reference"]}
if user is None: if user_entry is None:
# fallback to default way # no user, then no credentials
return DumbHTTPExtractor().extractCredentials(request) return {}
try: try:
self.setToken(self.prefix + token, user) self.setToken(token, user_entry)
except KeyError: except KeyError as error:
# allow to work w/o cache # allow to work w/o cache
pass LOG(self.getId(), ERROR, error)
# Credentials returned here will be used by ERP5LoginUserManager to find the login document # Credentials returned here will be used by ERP5LoginUserManager to find the login document
# having reference `user`. # having reference `user`.
creds = { creds = {
"login_portal_type": self.login_portal_type, "login_portal_type": self.login_portal_type,
"external_login": user "external_login": user_entry["reference"]
} }
# PAS wants remote_host / remote_address # PAS wants remote_host / remote_address
...@@ -196,8 +198,11 @@ class ERP5FacebookExtractionPlugin(ERP5ExternalOauth2ExtractionPlugin, BasePlugi ...@@ -196,8 +198,11 @@ class ERP5FacebookExtractionPlugin(ERP5ExternalOauth2ExtractionPlugin, BasePlugi
""" """
meta_type = "ERP5 Facebook Extraction Plugin" meta_type = "ERP5 Facebook Extraction Plugin"
prefix = 'fb_' cookie_name = "__ac_facebook_hash"
header_string = 'facebook' cache_factory_name = "facebook_server_auth_token_cache_factory"
def refreshTokenIfExpired(self, key, cache_value):
return cache_value
def getUserEntry(self, token): def getUserEntry(self, token):
if facebook is None: if facebook is None:
...@@ -234,12 +239,26 @@ class ERP5GoogleExtractionPlugin(ERP5ExternalOauth2ExtractionPlugin, BasePlugin) ...@@ -234,12 +239,26 @@ class ERP5GoogleExtractionPlugin(ERP5ExternalOauth2ExtractionPlugin, BasePlugin)
""" """
meta_type = "ERP5 Google Extraction Plugin" meta_type = "ERP5 Google Extraction Plugin"
prefix = 'go_'
header_string = 'google'
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):
expires_in = cache_value.get("token_response", {}).get("expires_in")
refresh_token = cache_value.get("refresh_token")
if expires_in and refresh_token:
if (time.time() - cache_value["response_timestamp"]) >= float(expires_in):
credential = oauth2client.client.OAuth2Credentials(
cache_value["access_token"], cache_value["client_id"],
cache_value["client_secret"], refresh_token,
cache_value["token_expiry"], cache_value["token_uri"],
cache_value["user_agent"])
response_data = credential.refresh(httplib2.Http())
cache_value.update(response_data)
cache_value["response_timestamp"] = time.time()
self.setToken(key, cache_value)
return cache_value
def getUserEntry(self, token): def getUserEntry(self, token):
if httplib2 is None: if httplib2 is None:
LOG('ERP5GoogleExtractionPlugin', INFO, LOG('ERP5GoogleExtractionPlugin', INFO,
...@@ -259,7 +278,6 @@ class ERP5GoogleExtractionPlugin(ERP5ExternalOauth2ExtractionPlugin, BasePlugin) ...@@ -259,7 +278,6 @@ class ERP5GoogleExtractionPlugin(ERP5ExternalOauth2ExtractionPlugin, BasePlugin)
google_entry = None google_entry = None
finally: finally:
socket.setdefaulttimeout(timeout) socket.setdefaulttimeout(timeout)
user_entry = {} user_entry = {}
if google_entry is not None: if google_entry is not None:
# sanitise value # sanitise value
......
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