Commit fba8d7a8 authored by Rafael Monnerat's avatar Rafael Monnerat

erp5_oauth_facebook_login: Implement Facebook Login Support for OAuth

   Changes on ERP5Security: Define getFacebookUserEntry to reduce code duplication
   Add facebook support for login and logout (optional) on erp5_core, xhtml and credentials.
parent b21bca15
...@@ -5,7 +5,8 @@ ...@@ -5,7 +5,8 @@
global form_id string:login_form; global form_id string:login_form;
available_oauth_login_list python: context.getPortalObject().ERP5Site_getAvailableOAuthLoginList(); available_oauth_login_list python: context.getPortalObject().ERP5Site_getAvailableOAuthLoginList();
enable_google_login python: 'google' in available_oauth_login_list; enable_google_login python: 'google' in available_oauth_login_list;
css_list python: enable_google_login and ['%s/zocial.min.css' % here.portal_url()] or []; enable_facebook_login python: 'facebook' in available_oauth_login_list;
css_list python: (enable_google_login or enable_facebook_login) and ['%s/zocial.min.css' % here.portal_url()] or [];
js_list python: ['%s/login_form.js' % (here.portal_url(), ), '%s/erp5.js' % (here.portal_url(), )]"> js_list python: ['%s/login_form.js' % (here.portal_url(), ), '%s/erp5.js' % (here.portal_url(), )]">
<tal:block metal:use-macro="here/main_template/macros/master"> <tal:block metal:use-macro="here/main_template/macros/master">
<tal:block metal:fill-slot="main"> <tal:block metal:fill-slot="main">
...@@ -59,6 +60,15 @@ ...@@ -59,6 +60,15 @@
</div> </div>
</div> </div>
</tal:block> </tal:block>
<tal:block tal:condition="enable_facebook_login">
<div class="field">
<label>&nbsp;</label>
<div class="input">
<a tal:attributes="href string:${here/portal_url}/ERP5Site_redirectToFacebookLoginPage"
i18n:translate="" i18n:domain="ui" class="zocial facebook">Login with Facebook</a>
</div>
</div>
</tal:block>
</fieldset> </fieldset>
<script type="text/javascript">setFocus()</script> <script type="text/javascript">setFocus()</script>
<p i18n:translate="" i18n:domain="ui">Having trouble logging in? Make sure to enable cookies in your web browser.</p> <p i18n:translate="" i18n:domain="ui">Having trouble logging in? Make sure to enable cookies in your web browser.</p>
......
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ActionInformation" module="Products.CMFCore.ActionInformation"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>action</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>categories</string> </key>
<value>
<tuple>
<string>action_type/object_view</string>
</tuple>
</value>
</item>
<item>
<key> <string>category</string> </key>
<value> <string>object_view</string> </value>
</item>
<item>
<key> <string>condition</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>icon</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>view</string> </value>
</item>
<item>
<key> <string>permissions</string> </key>
<value>
<tuple>
<string>View</string>
</tuple>
</value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Action Information</string> </value>
</item>
<item>
<key> <string>priority</string> </key>
<value> <float>1.0</float> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>View</string> </value>
</item>
<item>
<key> <string>visible</string> </key>
<value> <int>1</int> </value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="Expression" module="Products.CMFCore.Expression"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>text</string> </key>
<value> <string>string: ${object_url}/FacebookConnector_view</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
...@@ -2,10 +2,10 @@ import facebook ...@@ -2,10 +2,10 @@ import facebook
from Products.ERP5Security.ERP5ExternalOauth2ExtractionPlugin import getFacebookUserEntry from Products.ERP5Security.ERP5ExternalOauth2ExtractionPlugin import getFacebookUserEntry
def getAccessTokenFromCode(self, code, redirect_uri): def getAccessTokenFromCode(self, code, redirect_uri):
client_id, secret_key = self.ERP5Site_getFacebookClientIdAndSecretKey()
return facebook.GraphAPI(version="2.7").get_access_token_from_code( return facebook.GraphAPI(version="2.7").get_access_token_from_code(
code=code, redirect_uri=redirect_uri, code=code, redirect_uri=redirect_uri,
app_id=self.getClientId(), app_id=client_id, app_secret=secret_key)
app_secret=self.getSecretKey())
def getUserEntry(token): def getUserEntry(token):
return getFacebookUserEntry(token) return getFacebookUserEntry(token)
\ No newline at end of file
<allowed_content_type_list> <allowed_content_type_list>
<portal_type id="OAuth Tool">
<item>Facebook Connector</item>
</portal_type>
<portal_type id="Person"> <portal_type id="Person">
<item>Facebook Login</item> <item>Facebook Login</item>
</portal_type> </portal_type>
......
<property_sheet_list>
<portal_type id="Facebook Connector">
<item>OAuthClient</item>
</portal_type>
<portal_type id="Template Tool">
<item>TemplateToolERP5FacebookExtractionPluginConstraint</item>
</portal_type>
</property_sheet_list>
\ No newline at end of file
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Base Type" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>content_icon</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>Facebook Connector</string> </value>
</item>
<item>
<key> <string>init_script</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>permission</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Base Type</string> </value>
</item>
<item>
<key> <string>type_class</string> </key>
<value> <string>XMLObject</string> </value>
</item>
<item>
<key> <string>type_interface</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>type_mixin</string> </key>
<value>
<tuple/>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<workflow_chain> <workflow_chain>
<chain>
<type>Facebook Connector</type>
<workflow>edit_workflow, validation_workflow</workflow>
</chain>
<chain> <chain>
<type>Facebook Login</type> <type>Facebook Login</type>
<workflow>edit_workflow, validation_workflow</workflow> <workflow>edit_workflow, validation_workflow</workflow>
......
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Property Sheet" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_count</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>_mt_index</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
</value>
</item>
<item>
<key> <string>_tree</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAQ=</string> </persistent>
</value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>TemplateToolERP5FacebookExtractionPluginConstraint</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Property Sheet</string> </value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="Length" module="BTrees.Length"/>
</pickle>
<pickle> <int>0</int> </pickle>
</record>
<record id="3" aka="AAAAAAAAAAM=">
<pickle>
<global name="OOBTree" module="BTrees.OOBTree"/>
</pickle>
<pickle>
<none/>
</pickle>
</record>
<record id="4" aka="AAAAAAAAAAQ=">
<pickle>
<global name="OOBTree" module="BTrees.OOBTree"/>
</pickle>
<pickle>
<none/>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Script Constraint" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_identity_criterion</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>_range_criterion</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
</value>
</item>
<item>
<key> <string>categories</string> </key>
<value>
<tuple>
<string>constraint_type/post_upgrade</string>
</tuple>
</value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>ERP5FacebookExtractionPlugin_existence_constraint</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Script Constraint</string> </value>
</item>
<item>
<key> <string>script_id</string> </key>
<value> <string>TemplateTool_checkFacebookExtractionPluginExistenceConsistency</string> </value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="PersistentMapping" module="Persistence.mapping"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>data</string> </key>
<value>
<dictionary/>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="3" aka="AAAAAAAAAAM=">
<pickle>
<global name="PersistentMapping" module="Persistence.mapping"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>data</string> </key>
<value>
<dictionary/>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
...@@ -36,11 +36,11 @@ elif code is not None: ...@@ -36,11 +36,11 @@ elif code is not None:
{"reference": user_reference}, {"reference": user_reference},
"facebook_server_auth_token_cache_factory") "facebook_server_auth_token_cache_factory")
method = getattr(context, "Base_createOAuth2User", None) method = getattr(context, "ERP5Site_createFacebookUserToOAuth", None)
if method is not None: if method is not None:
pass #method("Facebook Login", user_reference, user_dict) method(user_reference, user_dict)
return context.REQUEST.RESPONSE.redirect( came_from = context.REQUEST.get("came_from", portal.absolute_url() + "#")
context.REQUEST.get("came_from") or portal.absolute_url()) return context.REQUEST.RESPONSE.redirect(came_from)
return handleError('') return handleError('')
...@@ -11,7 +11,7 @@ result_list = context.getPortalObject().portal_catalog( ...@@ -11,7 +11,7 @@ result_list = context.getPortalObject().portal_catalog(
assert result_list, "Facebook Connector not found" assert result_list, "Facebook Connector not found"
if len(result_list) == 2: if len(result_list) == 2:
raise ValueError("Impossible to select one Google Connector") raise ValueError("Impossible to select one Facebook Connector")
facebook_connector = result_list[0] facebook_connector = result_list[0]
return facebook_connector.getClientId(), facebook_connector.getSecretKey() return facebook_connector.getClientId(), facebook_connector.getSecretKey()
from ZTUtils import make_query from ZTUtils import make_query
client_id, _ = context.ERP5Site_getFacebookClientIdAndSecretKey()
query = make_query({ query = make_query({
# Call at he context of the appropriate web_service. # Call at he context of the appropriate web_service.
'client_id': context.getClientId(), 'client_id': client_id,
'redirect_uri': "{0}/ERP5Site_callbackFacebookLogin".format(came_from or context.absolute_url()), 'redirect_uri': "{0}/ERP5Site_callbackFacebookLogin".format(came_from or context.absolute_url()),
'scope': 'email' 'scope': 'email'
}) })
......
...@@ -30,16 +30,25 @@ from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase ...@@ -30,16 +30,25 @@ from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase
from erp5.component.extension import FacebookLoginUtility from erp5.component.extension import FacebookLoginUtility
from Products.ERP5Type.tests.utils import createZODBPythonScript from Products.ERP5Type.tests.utils import createZODBPythonScript
CLIENT_ID = "a1b2c3"
SECRET_KEY = "3c2ba1"
ACCESS_TOKEN = "EAAF10h0gIiQZDZD"
CODE = "1235"
def getUserId(access_token): def getUserId(access_token):
return "1209832093821098102938120938129381" return "1234567890123456"
def getAccessTokenFromCode(code, redirect_uri): def getAccessTokenFromCode(code, redirect_uri):
assert code == CODE, "Invalid code" assert code == CODE, "Invalid code"
# This is an example of a Facebook response # This is an example of a Facebook response
return {} return {u'access_token': u'EAAF10h0gIiQZDZD',
u'token_type': u'bearer',
u'expires_in': 5138578}
def getUserEntry(access_token): def getUserEntry(access_token):
return {} return {'name': 'John Doe',
'reference': getUserId(None),
'email': "dummy@example.org"}
FacebookLoginUtility_getAccessTokenFromCode = FacebookLoginUtility.getAccessTokenFromCode FacebookLoginUtility_getAccessTokenFromCode = FacebookLoginUtility.getAccessTokenFromCode
FacebookLoginUtility_getUserEntry = FacebookLoginUtility.getUserEntry FacebookLoginUtility_getUserEntry = FacebookLoginUtility.getUserEntry
...@@ -97,11 +106,11 @@ class TestFacebookLogin(ERP5TypeTestCase): ...@@ -97,11 +106,11 @@ class TestFacebookLogin(ERP5TypeTestCase):
self.logout() self.logout()
self.portal.ERP5Site_redirectToFacebookLoginPage() self.portal.ERP5Site_redirectToFacebookLoginPage()
location = self.portal.REQUEST.RESPONSE.getHeader("Location") location = self.portal.REQUEST.RESPONSE.getHeader("Location")
self.assertIn("XXXX", location) self.assertIn("https://www.facebook.com/v2.10/dialog/oauth?", location)
self.assertIn("response_type=code", location) self.assertIn("scope=email&redirect_uri=", location)
self.assertIn("client_id=%s" % CLIENT_ID, location) self.assertIn("client_id=%s" % CLIENT_ID, location)
self.assertNotIn("secret_key=", location) self.assertNotIn("secret_key=", location)
self.assertIn("ERP5Site_receiveFacebookCallback", location) self.assertIn("ERP5Site_callbackFacebookLogin", location)
def test_create_user_in_ERP5Site_createFacebookUserToOAuth(self): def test_create_user_in_ERP5Site_createFacebookUserToOAuth(self):
""" """
...@@ -143,8 +152,7 @@ return reference, None ...@@ -143,8 +152,7 @@ return reference, None
module = context.getPortalObject().getDefaultModule(portal_type='Credential Request') module = context.getPortalObject().getDefaultModule(portal_type='Credential Request')
credential_request = module.newContent( credential_request = module.newContent(
portal_type="Credential Request", portal_type="Credential Request",
first_name=user_dict["first_name"], first_name=user_dict["name"],
last_name=user_dict["last_name"],
reference=user_reference, reference=user_reference,
default_email_text=user_dict["email"], default_email_text=user_dict["email"],
) )
...@@ -153,12 +161,11 @@ context.portal_alarms.accept_submitted_credentials.activeSense() ...@@ -153,12 +161,11 @@ context.portal_alarms.accept_submitted_credentials.activeSense()
return credential_request return credential_request
""") """)
self.logout() self.logout()
response = self.portal.ERP5Site_receiveFacebookCallback(code=CODE) response = self.portal.ERP5Site_callbackFacebookLogin(code=CODE)
facebook_hash = self.portal.REQUEST.RESPONSE.cookies.get("__ac_facebook_hash")["value"] facebook_hash = self.portal.REQUEST.RESPONSE.cookies.get("__ac_facebook_hash")["value"]
self.assertEqual("b01533abb684a658dc71c81da4e67546", facebook_hash) self.assertEqual("8cec04e21e927f1023f4f4980ec11a77", facebook_hash)
self.assertEqual(self.portal.absolute_url(), response) self.assertEqual(self.portal.absolute_url(), response)
cache_dict = self.portal.Base_getBearerToken(facebook_hash, "facebook_server_auth_token_cache_factory") cache_dict = self.portal.Base_getBearerToken(facebook_hash, "facebook_server_auth_token_cache_factory")
self.assertEqual(CLIENT_ID, cache_dict["client_id"])
self.assertEqual(ACCESS_TOKEN, cache_dict["access_token"]) self.assertEqual(ACCESS_TOKEN, cache_dict["access_token"])
self.assertEqual({'reference': getUserId(None)}, self.assertEqual({'reference': getUserId(None)},
self.portal.Base_getBearerToken(ACCESS_TOKEN, "facebook_server_auth_token_cache_factory") self.portal.Base_getBearerToken(ACCESS_TOKEN, "facebook_server_auth_token_cache_factory")
...@@ -173,7 +180,8 @@ return credential_request ...@@ -173,7 +180,8 @@ return credential_request
self.login() self.login()
credential_request = self.portal.portal_catalog(portal_type="Credential Request", credential_request = self.portal.portal_catalog(portal_type="Credential Request",
reference=getUserId(None))[0].getObject() reference=getUserId(None))[0].getObject()
credential_request.accept() if credential_request.getValidationState() != "accepted":
credential_request.accept()
person = credential_request.getDestinationDecisionValue() person = credential_request.getDestinationDecisionValue()
facebook_login = person.objectValues(portal_types="Facebook Login")[0] facebook_login = person.objectValues(portal_types="Facebook Login")[0]
self.assertEqual(getUserId(None), facebook_login.getReference()) self.assertEqual(getUserId(None), facebook_login.getReference())
...@@ -45,7 +45,9 @@ ...@@ -45,7 +45,9 @@
<item> <item>
<key> <string>text_content_warning_message</string> </key> <key> <string>text_content_warning_message</string> </key>
<value> <value>
<tuple/> <tuple>
<string>W: 77, 4: Unused variable \'person_module\' (unused-variable)</string>
</tuple>
</value> </value>
</item> </item>
<item> <item>
......
Facebook Connector | view
Facebook Login | view Facebook Login | view
\ No newline at end of file
OAuth Tool | Facebook Connector
Person | Facebook Login Person | Facebook Login
\ No newline at end of file
Facebook Connector
Facebook Login Facebook Login
\ No newline at end of file
Facebook Connector | OAuthClient
Template Tool | TemplateToolERP5FacebookExtractionPluginConstraint
\ No newline at end of file
Facebook Connector | edit_workflow
Facebook Connector | validation_workflow
Facebook Login | edit_workflow Facebook Login | edit_workflow
Facebook Login | validation_workflow Facebook Login | validation_workflow
\ No newline at end of file
TemplateToolERP5FacebookExtractionPluginConstraint
\ No newline at end of file
erp5_full_text_myisam_catalog erp5_full_text_myisam_catalog
\ No newline at end of file erp5_credential
...@@ -15,4 +15,7 @@ REQUEST.RESPONSE.expireCookie('__ac', path='/') ...@@ -15,4 +15,7 @@ REQUEST.RESPONSE.expireCookie('__ac', path='/')
if getattr(portal.portal_skins, "erp5_oauth_google_login", None): if getattr(portal.portal_skins, "erp5_oauth_google_login", None):
REQUEST.RESPONSE.expireCookie('__ac_google_hash', path='/') REQUEST.RESPONSE.expireCookie('__ac_google_hash', path='/')
if getattr(portal.portal_skins, "erp5_oauth_facebook_login", None):
REQUEST.RESPONSE.expireCookie('__ac_facebook_hash', path='/')
return REQUEST.RESPONSE.redirect(REQUEST.URL1 + '/logged_out') return REQUEST.RESPONSE.redirect(REQUEST.URL1 + '/logged_out')
...@@ -5,4 +5,8 @@ portal_skin = context.getPortalObject().portal_skins ...@@ -5,4 +5,8 @@ portal_skin = context.getPortalObject().portal_skins
if getattr(portal_skin, "erp5_oauth_google_login", None) is not None: if getattr(portal_skin, "erp5_oauth_google_login", None) is not None:
oauth_login_list.append("google") oauth_login_list.append("google")
if getattr(portal_skin, "erp5_oauth_facebook_login", None) is not None:
oauth_login_list.append("facebook")
return oauth_login_list return oauth_login_list
...@@ -5,7 +5,8 @@ ...@@ -5,7 +5,8 @@
global form_id string:login_form; global form_id string:login_form;
available_oauth_login_list python: context.getPortalObject().ERP5Site_getAvailableOAuthLoginList(); available_oauth_login_list python: context.getPortalObject().ERP5Site_getAvailableOAuthLoginList();
enable_google_login python: 'google' in available_oauth_login_list; enable_google_login python: 'google' in available_oauth_login_list;
css_list python: enable_google_login and ['%s/zocial.min.css' % here.portal_url()] or []; enable_facebook_login python: 'facebook' in available_oauth_login_list;
css_list python: (enable_google_login or enable_facebook_login) and ['%s/zocial.min.css' % here.portal_url()] or [];
js_list python: ['%s/login_form.js' % (here.portal_url(), ), '%s/erp5.js' % (here.portal_url(), )]"> js_list python: ['%s/login_form.js' % (here.portal_url(), ), '%s/erp5.js' % (here.portal_url(), )]">
<tal:block metal:use-macro="here/main_template/macros/master"> <tal:block metal:use-macro="here/main_template/macros/master">
<tal:block metal:fill-slot="main"> <tal:block metal:fill-slot="main">
...@@ -58,6 +59,15 @@ ...@@ -58,6 +59,15 @@
</div> </div>
</div> </div>
</tal:block> </tal:block>
<tal:block tal:condition="enable_facebook_login">
<div class="field">
<label>&nbsp;</label>
<div class="input">
<a tal:attributes="href string:${here/portal_url}/ERP5Site_redirectToFacebookLoginPage"
i18n:translate="" i18n:domain="ui" class="zocial facebook">Login with Facebook</a>
</div>
</div>
</tal:block>
</fieldset> </fieldset>
<script type="text/javascript">setFocus()</script> <script type="text/javascript">setFocus()</script>
<p i18n:translate="" i18n:domain="ui">Having trouble logging in? Make sure to enable cookies in your web browser.</p> <p i18n:translate="" i18n:domain="ui">Having trouble logging in? Make sure to enable cookies in your web browser.</p>
......
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