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 httplib2
import apiclient.discovery
......@@ -7,36 +5,33 @@ import oauth2client.client
import socket
from zLOG import LOG, ERROR
def getAccessTokenFromCode(self, code, redirect_uri):
connection_kw = {'host': 'accounts.google.com', 'timeout': 30}
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
SCOPE_LIST = ['https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/userinfo.email']
try:
body = json.loads(response.read())
except Exception, error_str:
return status, {"error": error_str}
def redirectToGoogleLoginPage(self):
portal = self.getPortalObject()
flow = oauth2client.client.OAuth2WebServerFlow(
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:
return status, body
except Exception:
return status, None
def getAccessTokenFromCode(self, code, redirect_uri):
portal = self.getPortalObject()
flow = oauth2client.client.OAuth2WebServerFlow(
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):
timeout = socket.getdefaulttimeout()
......
......@@ -26,7 +26,7 @@
</item>
<item>
<key> <string>cache_duration</string> </key>
<value> <int>3600</int> </value>
<value> <int>86400</int> </value>
</item>
<item>
<key> <string>description</string> </key>
......
......@@ -2,17 +2,33 @@
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Ram Cache" module="erp5.portal_type"/>
<global name="Distributed Ram Cache" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>categories</string> </key>
<value>
<tuple>
<string>specialise/portal_memcached/persistent_memcached_plugin</string>
</tuple>
</value>
</item>
<item>
<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>
<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>
</dictionary>
</pickle>
......
import time
def handleError(error):
context.Base_redirect(
'login_form',
......@@ -9,22 +11,24 @@ def handleError(error):
if error is not None:
return handleError(error)
elif code is not None:
portal = context.getPortalObject()
status, response_dict = context.ERP5Site_getAccessTokenFromCode(
response_dict = context.ERP5Site_getAccessTokenFromCode(
code,
"{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:
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)
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,
response_dict,
"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(
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 @@
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="PythonScript" module="Products.PythonScripts.PythonScript"/>
<global name="ExternalMethod" module="Products.ExternalMethod.ExternalMethod"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>Script_magic</string> </key>
<value> <int>3</int> </value>
<key> <string>_function</string> </key>
<value> <string>redirectToGoogleLoginPage</string> </value>
</item>
<item>
<key> <string>_bind_names</string> </key>
<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>
<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>
<key> <string>_params</string> </key>
<value> <string></string> </value>
<key> <string>_module</string> </key>
<value> <string>GoogleLoginUtility</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>ERP5Site_redirectToGoogleLoginPage</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
......
......@@ -34,11 +34,14 @@ from Products.PluggableAuthService.interfaces import plugins
from Products.PluggableAuthService.utils import classImplements
from Products.PluggableAuthService.plugins.BasePlugin import BasePlugin
from Products import ERP5Security
from Products.PluggableAuthService.PluggableAuthService import DumbHTTPExtractor
from AccessControl.SecurityManagement import getSecurityManager, \
setSecurityManager, newSecurityManager
from Products.ERP5Type.Cache import DEFAULT_CACHE_SCOPE
import time
import socket
import httplib
import urllib
import json
from zLOG import LOG, ERROR, INFO
try:
......@@ -115,7 +118,7 @@ class ERP5ExternalOauth2ExtractionPlugin:
cache_tool.updateCache()
cache_factory = cache_tool.getRamCacheRoot().get(self.cache_factory_name)
if cache_factory is None:
raise KeyError
raise KeyError("Cache Factory %s not found" % self.cache_factory)
return cache_factory
def setToken(self, key, body):
......@@ -130,7 +133,7 @@ class ERP5ExternalOauth2ExtractionPlugin:
for cache_plugin in cache_factory.getCachePluginList():
cache_entry = cache_plugin.get(key, DEFAULT_CACHE_SCOPE)
if cache_entry is not None:
return cache_entry.getValue()
return self.refreshTokenIfExpired(key, cache_entry.getValue())
raise KeyError('Key %r not found' % key)
####################################
......@@ -145,41 +148,40 @@ class ERP5ExternalOauth2ExtractionPlugin:
user_dict = self.getToken(cookie_hash)
except KeyError:
LOG(self.getId(), INFO, 'Hash %s not found' % cookie_hash)
return DumbHTTPExtractor().extractCredentials(request)
return {}
token = None
if "access_token" in user_dict:
token = user_dict["access_token"]
if token is None:
# no token
return DumbHTTPExtractor().extractCredentials(request)
# no token, then no credentials
return {}
# token is available
user = None
user_entry = None
try:
user = self.getToken(self.prefix + token)
user_entry = self.getToken(token)
except KeyError:
user_entry = self.getUserEntry(token)
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:
# fallback to default way
return DumbHTTPExtractor().extractCredentials(request)
if user_entry is None:
# no user, then no credentials
return {}
try:
self.setToken(self.prefix + token, user)
except KeyError:
self.setToken(token, user_entry)
except KeyError as error:
# 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
# having reference `user`.
creds = {
"login_portal_type": self.login_portal_type,
"external_login": user
"external_login": user_entry["reference"]
}
# PAS wants remote_host / remote_address
......@@ -196,8 +198,11 @@ class ERP5FacebookExtractionPlugin(ERP5ExternalOauth2ExtractionPlugin, BasePlugi
"""
meta_type = "ERP5 Facebook Extraction Plugin"
prefix = 'fb_'
header_string = 'facebook'
cookie_name = "__ac_facebook_hash"
cache_factory_name = "facebook_server_auth_token_cache_factory"
def refreshTokenIfExpired(self, key, cache_value):
return cache_value
def getUserEntry(self, token):
if facebook is None:
......@@ -234,12 +239,26 @@ class ERP5GoogleExtractionPlugin(ERP5ExternalOauth2ExtractionPlugin, BasePlugin)
"""
meta_type = "ERP5 Google Extraction Plugin"
prefix = 'go_'
header_string = 'google'
login_portal_type = "Google Login"
cookie_name = "__ac_google_hash"
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):
if httplib2 is None:
LOG('ERP5GoogleExtractionPlugin', INFO,
......@@ -259,7 +278,6 @@ class ERP5GoogleExtractionPlugin(ERP5ExternalOauth2ExtractionPlugin, BasePlugin)
google_entry = None
finally:
socket.setdefaulttimeout(timeout)
user_entry = {}
if google_entry is not None:
# 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