Commit 8fd74907 authored by Cédric Le Ninivin's avatar Cédric Le Ninivin

Introduce OpenId Connect

See merge request !1501
parents 9fbfeaac 9c4d0edd
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
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;
enable_facebook_login python: 'facebook' in available_oauth_login_list; enable_facebook_login python: 'facebook' in available_oauth_login_list;
enable_openidconnect_login python: 'openidconnect' in available_oauth_login_list;
css_list python: (enable_google_login or enable_facebook_login) and ['%s/zocial.min.css' % here.portal_url()] or []; 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">
...@@ -71,6 +72,15 @@ ...@@ -71,6 +72,15 @@
</div> </div>
</div> </div>
</tal:block> </tal:block>
<tal:block tal:condition="enable_openidconnect_login">
<div class="field">
<label>&nbsp;</label>
<div class="input">
<a tal:attributes="href string:${here/portal_url}/ERP5Site_redirectToOpenIdLoginPage"
i18n:translate="" i18n:domain="ui" class="zocial openid">Login with OpenId Connect</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}/OpenIdConnectConnector_view</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?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}/ExternalLogin_view</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
# -*- coding:utf-8 -*-
##############################################################################
#
# Copyright (C) 2021 Nexedi SA and Contributors.
#
# This program is free software: you can Use, Study, Modify and Redistribute
# it under the terms of the GNU General Public License version 3, or (at your
# option) any later version, as published by the Free Software Foundation.
#
# You can also Link and Combine this program with other software covered by
# the terms of any of the Free Software licenses or any of the Open Source
# Initiative approved licenses and Convey the resulting work. Corresponding
# source of such a combination shall include the source code for all other
# software used.
#
# This program is distributed WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# See COPYING file for full licensing terms.
# See https://www.nexedi.com/licensing for rationale and options.
#
##############################################################################
import json
from oic import rndstr
from oic.oauth2.grant import Token
from oic.oic import Client
from oic.oic.message import AuthorizationResponse
from oic.oic.message import RegistrationResponse
from zExceptions import Unauthorized
openid_connect_cache_factory = "openid_connect_server_auth_token_cache_factory"
def _getOpenOpenIdConnector(portal, reference="default"):
"""Returns google client id and secret key.
Internal function.
"""
result_list = portal.portal_catalog.unrestrictedSearchResults(
portal_type="OpenId Connect Connector",
reference=reference,
validation_state="validated",
limit=2,
)
assert result_list, "OpenId Connector not found"
if len(result_list) == 2:
raise ValueError("Impossible to select one OpenId Connector Please contact support")
openid_connector = result_list[0].getObject()
return openid_connector
def unrestrictedSearchOpenIdConnectLogin(self, login, REQUEST=None):
if REQUEST is not None:
raise Unauthorized
return self.getPortalObject().portal_catalog.unrestrictedSearchResults(
portal_type="OpenId Connect Login",
reference=login,
validation_state="validated", limit=1)
def _getOpenOpenIdClientIdAndSecretKey(portal, reference="default"):
"""Returns client id and secret key.
Internal function.
"""
openid_connector = _getOpenOpenIdConnector(portal, reference)
return openid_connector.getUserId(), openid_connector.getPassword()
def _prepareAndReturnClient(portal, openid_connector, reference="default"):
from oic.utils.authn.client import CLIENT_AUTHN_METHOD
client = Client(client_authn_method=CLIENT_AUTHN_METHOD)
issuer = openid_connector.getUrlString()
client.provider_config(issuer)
client_metadata = json.loads(openid_connector.getDescription())
client_metadata["client_id"] = openid_connector.getUserId()
client_metadata["client_secret"] = openid_connector.getPassword()
client_reg = RegistrationResponse(**client_metadata)
client.store_registration_info(client_reg)
return client
def redirectToOpenIdConnectLoginPage(self, reference="default"):
portal = self.getPortalObject()
openid_connector = _getOpenOpenIdConnector(portal, reference)
client = _prepareAndReturnClient(portal, openid_connector, reference)
session = {}
session["state"] = rndstr()
session["nonce"] = rndstr()
portal.Base_setBearerToken(session["state"], session, openid_connect_cache_factory)
args = {
"client_id": client.client_id,
"response_type": "code",
"scope": openid_connector.getScopeList(),
"nonce": session["nonce"],
"redirect_uri": client.registration_response["redirect_uris"][0],
"state": session["state"]
}
auth_req = client.construct_AuthorizationRequest(request_args=args)
login_url = auth_req.request(client.authorization_endpoint)
return self.REQUEST.RESPONSE.redirect(login_url)
def getAccessTokenFromCode(self, query_string, redirect_uri, reference="default"):
portal = self.getPortalObject()
openid_connector = _getOpenOpenIdConnector(portal, reference)
client = _prepareAndReturnClient(portal, openid_connector, reference)
aresp = client.parse_response(
AuthorizationResponse,
info=query_string,
sformat="urlencoded"
)
args = {
"redirect_uri": client.registration_response["redirect_uris"][0],
"grant_type": "authorization_code",
}
response = client.do_access_token_request(
state=aresp["state"],
request_args=args,
authn_method="client_secret_basic",
)
response = dict(response)
if 'id_token' in response:
assert response['id_token'].verify()
response.pop('id_token')
return response
def getAccessTokenFromRefreshToken(self, response_dict, reference="default"):
portal = self.getPortalObject()
openid_connector = _getOpenOpenIdConnector(portal, reference)
client = _prepareAndReturnClient(portal, openid_connector, reference)
args = {
"redirect_uri": client.registration_response["redirect_uris"][0],
}
token = Token(response_dict)
response = client.do_access_token_refresh(
token=token,
request_args=args,
authn_method="client_secret_basic",
)
if 'id_token' in response:
assert response['id_token'].verify()
return dict(response)
def getUserEntry(self, token="", reference="default"):
portal = self.getPortalObject()
openid_connector = _getOpenOpenIdConnector(portal, reference)
client = _prepareAndReturnClient(portal, openid_connector, reference)
result = client.do_user_info_request(token=token, method="GET")
return dict(result)
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Extension Component" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_recorded_property_dict</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>default_reference</string> </key>
<value> <string>OpenIdConnectLoginUtility</string> </value>
</item>
<item>
<key> <string>default_source_reference</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>extension.erp5.OpenIdConnectLoginUtility</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Extension Component</string> </value>
</item>
<item>
<key> <string>sid</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>text_content_error_message</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>text_content_warning_message</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>version</string> </key>
<value> <string>erp5</string> </value>
</item>
<item>
<key> <string>workflow_history</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
</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>
<item>
<key> <string>component_validation_workflow</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAQ=</string> </persistent>
</value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="4" aka="AAAAAAAAAAQ=">
<pickle>
<global name="WorkflowHistoryList" module="Products.ERP5Type.Workflow"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_log</string> </key>
<value>
<list>
<dictionary>
<item>
<key> <string>action</string> </key>
<value> <string>validate</string> </value>
</item>
<item>
<key> <string>validation_state</string> </key>
<value> <string>validated</string> </value>
</item>
</dictionary>
</list>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Cache Factory" 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>cache_duration</string> </key>
<value> <int>86400</int> </value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>openid_connect_server_auth_token_cache_factory</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Cache Factory</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>openid_connect_server_auth_token_cache_factory</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="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>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>Distributed Ram Cache</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>persistent_cache</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<allowed_content_type_list>
<portal_type id="Person">
<item>OpenId Connect Login</item>
</portal_type>
<portal_type id="Web Service Tool">
<item>OpenId Connect Connector</item>
</portal_type>
</allowed_content_type_list>
\ No newline at end of file
<property_sheet_list>
<portal_type id="OpenId Connect Connector">
<item>Login</item>
<item>OpenIdConnectConnector</item>
<item>Reference</item>
<item>Url</item>
</portal_type>
<portal_type id="Template Tool">
<item>TemplateToolERP5OpenIdConnectExtractionPluginConstraint</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>OpenId Connect 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>
<?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>group_list</string> </key>
<value>
<tuple>
<string>login</string>
</tuple>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>OpenId Connect Login</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>searchable_text_property_id</string> </key>
<value>
<tuple>
<string>reference</string>
</tuple>
</value>
</item>
<item>
<key> <string>type_class</string> </key>
<value> <string>Login</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>
<chain>
<type>OpenId Connect Connector</type>
<workflow>edit_workflow, validation_workflow</workflow>
</chain>
<chain>
<type>OpenId Connect Login</type>
<workflow>edit_workflow, login_interaction_workflow, validation_workflow</workflow>
</chain>
</workflow_chain>
\ No newline at end of file
<?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>OpenIdConnectConnector</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="Standard Property" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>categories</string> </key>
<value>
<tuple>
<string>elementary_type/lines</string>
</tuple>
</value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>scope_property</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Standard Property</string> </value>
</item>
<item>
<key> <string>property_default</string> </key>
<value> <string>python: ()</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Folder" module="OFS.Folder"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_objects</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>erp5_openid_connect_client</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>getAccessTokenFromRefreshToken</string> </value>
</item>
<item>
<key> <string>_module</string> </key>
<value> <string>OpenIdConnectLoginUtility</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>ERP5Site_getAccessTokenFromRefreshToken</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>getAccessTokenFromCode</string> </value>
</item>
<item>
<key> <string>_module</string> </key>
<value> <string>OpenIdConnectLoginUtility</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>ERP5Site_getOpenIdConnectAccessTokenFromCode</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>unrestrictedSearchOpenIdConnectLogin</string> </value>
</item>
<item>
<key> <string>_module</string> </key>
<value> <string>OpenIdConnectLoginUtility</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>ERP5Site_getOpenIdConnectLogin</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>OpenIdConnectLoginUtility</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>ERP5Site_getOpenIdUserEntry</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_getOpenIdConnectLogin(login)
if login is None or not len(login):
return None
if len(login) > 1:
raise ValueError("Duplicated User")
return login[0].getParentValue().getRelativeUrl()
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="PythonScript" module="Products.PythonScripts.PythonScript"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>Script_magic</string> </key>
<value> <int>3</int> </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>login, REQUEST=None</string> </value>
</item>
<item>
<key> <string>_proxy_roles</string> </key>
<value>
<tuple>
<string>Manager</string>
</tuple>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>ERP5Site_getPersonFromOpenIdLogin</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
import time
request = container.REQUEST
response = request.RESPONSE
cache_factory = "openid_connect_server_auth_token_cache_factory"
def handleError(error, error_description="", state=None):
if state:
context.Base_setBearerToken(state, None, cache_factory)
return context.getWebSiteValue().Base_redirect(
'login_form',
keep_items={"portal_status_message":
context.Base_translateString(
"There was problem with your login: ${error} ${error_description}. Please contact your administrator.",
mapping={"error": error, "error_description": error_description})
})
state = state or request.form["state"]
if not state:
raise ValueError("Missing state value")
session = context.Base_getBearerToken(state, cache_factory)
if not session:
raise ValueError("Unknown state Value")
error = error or request.form.get("error", None)
error_description = error_description or request.form.get("error_description", None)
code = code or request.form.get("code", None)
if error is not None:
return handleError(error, error_description, state)
elif code is not None:
response_dict = context.ERP5Site_getOpenIdConnectAccessTokenFromCode(
context.REQUEST.environ['QUERY_STRING'],
"{0}/hateoas/connection/oid_auth".format(context.absolute_url())
)
if response_dict is not None:
"""
Here is an example of correct response dict:
{
'access_token': u'XXXX0koYI5hASXZaExeYsGlqz1bSIcGyEg',
'token_type': u'Bearer',
'expires_in': 3599,
'refresh_token': u'XXXXXXQocR4zlxQUdrit5sZ1FutZcece9'
}
"""
if "error" in response_dict:
return handleError(response_dict.get('error'), response_dict.get('error_description'), state)
access_token = response_dict['access_token'].encode('utf-8')
hash_str = context.Base_getHMAC(access_token, access_token)
context.setAuthCookie(response, '__ac_openidconnect_hash', hash_str)
# store timestamp in second since the epoch in UTC is enough
response_dict["response_timestamp"] = time.time()
context.Base_setBearerToken(hash_str,
response_dict,
"openid_connect_server_auth_token_cache_factory")
user_dict = context.ERP5Site_getOpenIdUserEntry(token=access_token)
user_reference = user_dict["sub"].encode('utf-8')
context.Base_setBearerToken(access_token,
{"reference": user_reference},
"openid_connect_server_auth_token_cache_factory")
# XXX for ERP5JS web sites without a rewrite rule, we make sure there's a trailing /
web_site_value = context.getWebSiteValue() or context
person_relative_url = context.ERP5Site_getPersonFromOpenIdLogin(user_reference)
if not person_relative_url:
method = getattr(context, "ERP5Site_createOpenIdConnectUserToOAuth", None)
if method is not None:
method(user_reference, user_dict)
# XXX CLN Hackish redirect
return web_site_value.Base_redirect('hateoas/connection/login_form', keep_items={
"portal_status_message": "Your user is being created, please retry clicking on 'Login with OpenId Connect' in 1 minute."
})
came_from = web_site_value.absolute_url() + "/#!login?n.me=%s" % person_relative_url
response.setHeader('Location', came_from)
response.setStatus(303)
else:
return handleError('')
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="PythonScript" module="Products.PythonScripts.PythonScript"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>Script_magic</string> </key>
<value> <int>3</int> </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>code=None, error=None, error_description=None, state=None</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>ERP5Site_receiveOpenIdCallback</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>redirectToOpenIdConnectLoginPage</string> </value>
</item>
<item>
<key> <string>_module</string> </key>
<value> <string>OpenIdConnectLoginUtility</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>ERP5Site_redirectToOpenIdLoginPage</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="ERP5 Form" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_objects</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>action</string> </key>
<value> <string>Base_edit</string> </value>
</item>
<item>
<key> <string>action_title</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>edit_order</string> </key>
<value>
<list/>
</value>
</item>
<item>
<key> <string>encoding</string> </key>
<value> <string>UTF-8</string> </value>
</item>
<item>
<key> <string>enctype</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>group_list</string> </key>
<value>
<list>
<string>left</string>
<string>right</string>
<string>center</string>
<string>bottom</string>
<string>hidden</string>
</list>
</value>
</item>
<item>
<key> <string>groups</string> </key>
<value>
<dictionary>
<item>
<key> <string>bottom</string> </key>
<value>
<list/>
</value>
</item>
<item>
<key> <string>center</string> </key>
<value>
<list>
<string>my_scope_list</string>
<string>my_description</string>
</list>
</value>
</item>
<item>
<key> <string>hidden</string> </key>
<value>
<list/>
</value>
</item>
<item>
<key> <string>left</string> </key>
<value>
<list>
<string>my_reference</string>
<string>my_url_string</string>
</list>
</value>
</item>
<item>
<key> <string>right</string> </key>
<value>
<list>
<string>my_user_id</string>
<string>my_password</string>
<string>my_translated_validation_state_title</string>
</list>
</value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>OpenIdConnectConnector_view</string> </value>
</item>
<item>
<key> <string>method</string> </key>
<value> <string>POST</string> </value>
</item>
<item>
<key> <string>name</string> </key>
<value> <string>OpenIdConnectConnector_view</string> </value>
</item>
<item>
<key> <string>pt</string> </key>
<value> <string>form_view</string> </value>
</item>
<item>
<key> <string>row_length</string> </key>
<value> <int>4</int> </value>
</item>
<item>
<key> <string>stored_encoding</string> </key>
<value> <string>UTF-8</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>OpenId Connect Connector</string> </value>
</item>
<item>
<key> <string>unicode_mode</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>update_action</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>update_action_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="ProxyField" module="Products.ERP5Form.ProxyField"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>delegated_list</string> </key>
<value>
<list>
<string>title</string>
</list>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>my_description</string> </value>
</item>
<item>
<key> <string>message_values</string> </key>
<value>
<dictionary>
<item>
<key> <string>external_validator_failed</string> </key>
<value> <string>The input failed the external validator.</string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>overrides</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>target</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>tales</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>target</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>values</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string>my_text_area_field</string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string>Base_viewFieldLibrary</string> </value>
</item>
<item>
<key> <string>target</string> </key>
<value> <string>Click to edit the target</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Client JSON Metadata</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="PasswordField" module="Products.Formulator.StandardFields"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>id</string> </key>
<value> <string>my_password</string> </value>
</item>
<item>
<key> <string>message_values</string> </key>
<value>
<dictionary>
<item>
<key> <string>external_validator_failed</string> </key>
<value> <string>The input failed the external validator.</string> </value>
</item>
<item>
<key> <string>required_not_found</string> </key>
<value> <string>Input is required but no input given.</string> </value>
</item>
<item>
<key> <string>too_long</string> </key>
<value> <string>Too much input was given.</string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>overrides</string> </key>
<value>
<dictionary>
<item>
<key> <string>alternate_name</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>css_class</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>default</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>display_maxwidth</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>display_width</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>editable</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>enabled</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>external_validator</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>extra</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>hidden</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>max_length</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>required</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>truncate</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>unicode</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>whitespace_preserve</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>tales</string> </key>
<value>
<dictionary>
<item>
<key> <string>alternate_name</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>css_class</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>default</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>display_maxwidth</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>display_width</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>editable</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>enabled</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>external_validator</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>extra</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>hidden</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>max_length</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>required</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>truncate</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>unicode</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>whitespace_preserve</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>values</string> </key>
<value>
<dictionary>
<item>
<key> <string>alternate_name</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>css_class</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>default</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>display_maxwidth</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>display_width</string> </key>
<value> <int>20</int> </value>
</item>
<item>
<key> <string>editable</string> </key>
<value> <int>1</int> </value>
</item>
<item>
<key> <string>enabled</string> </key>
<value> <int>1</int> </value>
</item>
<item>
<key> <string>external_validator</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>extra</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>hidden</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>input_type</string> </key>
<value> <string>text</string> </value>
</item>
<item>
<key> <string>max_length</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>required</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Secret Key</string> </value>
</item>
<item>
<key> <string>truncate</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>unicode</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>whitespace_preserve</string> </key>
<value> <int>0</int> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ProxyField" module="Products.ERP5Form.ProxyField"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>id</string> </key>
<value> <string>my_reference</string> </value>
</item>
<item>
<key> <string>message_values</string> </key>
<value>
<dictionary>
<item>
<key> <string>external_validator_failed</string> </key>
<value> <string>The input failed the external validator.</string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>overrides</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>tales</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>values</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string>my_reference</string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string>Base_viewFieldLibrary</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="LinesField" module="Products.Formulator.StandardFields"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>id</string> </key>
<value> <string>my_scope_list</string> </value>
</item>
<item>
<key> <string>message_values</string> </key>
<value>
<dictionary>
<item>
<key> <string>external_validator_failed</string> </key>
<value> <string>The input failed the external validator.</string> </value>
</item>
<item>
<key> <string>line_too_long</string> </key>
<value> <string>A line was too long.</string> </value>
</item>
<item>
<key> <string>required_not_found</string> </key>
<value> <string>Input is required but no input given.</string> </value>
</item>
<item>
<key> <string>too_long</string> </key>
<value> <string>You entered too many characters.</string> </value>
</item>
<item>
<key> <string>too_many_lines</string> </key>
<value> <string>You entered too many lines.</string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>overrides</string> </key>
<value>
<dictionary>
<item>
<key> <string>alternate_name</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>css_class</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>default</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>editable</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>enabled</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>external_validator</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>extra</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>height</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>hidden</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>max_length</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>max_linelength</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>max_lines</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>required</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>unicode</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>view_separator</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>whitespace_preserve</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>width</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>tales</string> </key>
<value>
<dictionary>
<item>
<key> <string>alternate_name</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>css_class</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>default</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>editable</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>enabled</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>external_validator</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>extra</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>height</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>hidden</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>max_length</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>max_linelength</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>max_lines</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>required</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>unicode</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>view_separator</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>whitespace_preserve</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>width</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>values</string> </key>
<value>
<dictionary>
<item>
<key> <string>alternate_name</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>css_class</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>default</string> </key>
<value>
<list/>
</value>
</item>
<item>
<key> <string>description</string> </key>
<value> <string>Scope of the OpenId Connect Connector. Use one element per line.</string> </value>
</item>
<item>
<key> <string>editable</string> </key>
<value> <int>1</int> </value>
</item>
<item>
<key> <string>enabled</string> </key>
<value> <int>1</int> </value>
</item>
<item>
<key> <string>external_validator</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>extra</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>height</string> </key>
<value> <int>5</int> </value>
</item>
<item>
<key> <string>hidden</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>max_length</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>max_linelength</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>max_lines</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>required</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Scope</string> </value>
</item>
<item>
<key> <string>unicode</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>view_separator</string> </key>
<value> <string encoding="cdata"><![CDATA[
<br />
]]></string> </value>
</item>
<item>
<key> <string>whitespace_preserve</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>width</string> </key>
<value> <int>40</int> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ProxyField" module="Products.ERP5Form.ProxyField"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>delegated_list</string> </key>
<value>
<list/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>my_translated_validation_state_title</string> </value>
</item>
<item>
<key> <string>message_values</string> </key>
<value>
<dictionary>
<item>
<key> <string>external_validator_failed</string> </key>
<value> <string>The input failed the external validator.</string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>overrides</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>tales</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>values</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string>my_translated_workflow_state_title</string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string>Base_viewFieldLibrary</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ProxyField" module="Products.ERP5Form.ProxyField"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>delegated_list</string> </key>
<value>
<list>
<string>title</string>
</list>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>my_url_string</string> </value>
</item>
<item>
<key> <string>message_values</string> </key>
<value>
<dictionary>
<item>
<key> <string>external_validator_failed</string> </key>
<value> <string>The input failed the external validator.</string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>overrides</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>target</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>tales</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>target</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>values</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string>my_string_field</string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string>Base_viewFieldLibrary</string> </value>
</item>
<item>
<key> <string>target</string> </key>
<value> <string>Click to edit the target</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Provider Metadata URL / issuer</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ProxyField" module="Products.ERP5Form.ProxyField"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>delegated_list</string> </key>
<value>
<list>
<string>title</string>
</list>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>my_user_id</string> </value>
</item>
<item>
<key> <string>message_values</string> </key>
<value>
<dictionary>
<item>
<key> <string>external_validator_failed</string> </key>
<value> <string>The input failed the external validator.</string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>overrides</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>target</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>tales</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>target</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>values</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string>my_string_field</string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string>Base_viewFieldLibrary</string> </value>
</item>
<item>
<key> <string>target</string> </key>
<value> <string>Click to edit the target</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Client Id</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
acl_users = context.getPortalObject().acl_users
plugin_id = 'erp5_openid_connect_extraction'
error_list = []
if plugin_id not in acl_users.objectIds():
error_list.append(
'OpenId Connect Extraction Plugin does not exist as %s/%s' % (acl_users.getPath(), plugin_id))
if fixit:
acl_users.manage_addProduct['ERP5Security'].addERP5OpenIdConnectExtractionPlugin(plugin_id)
getattr(acl_users, plugin_id).manage_activateInterfaces([
'IExtractionPlugin',
])
return error_list
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="PythonScript" module="Products.PythonScripts.PythonScript"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>Script_magic</string> </key>
<value> <int>3</int> </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>fixit=False</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>TemplateTool_checkOpenIdConnectExtractionPluginExistenceConsistency</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
##############################################################################
#
# Copyright (c) 2002-2016 Nexedi SA and Contributors. All Rights Reserved.
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsibility 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
# guarantees 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
##############################################################################
import uuid
import mock
import lxml
from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase
from Products.ERP5Type.tests.utils import createZODBPythonScript
CLIENT_ID = "a1b2c3"
SECRET_KEY = "3c2ba1"
SCOPE = ('username', 'openid', 'prd:test')
URL_STRING = 'https://openidtest.example.org:443'
DESCRIPTION = """{
"redirect_uris": ["https://testdomain.erp5.net/hateoas/connection/oid_auth"],
"response_types": "form_post",
"contacts": ["testuser@nexedi.com"],
"client_name": "test"
}"""
ACCESS_TOKEN = "XXXX0koYI5hASXZaExeYsGlqz1bSIcGyEg"
CODE = "1234"
def getUserId(access_token):
return "ETEST234"
def getAccessTokenFromCode(code, redirect_uri):
# This is an example of an OpenId response
return {
'access_token': u'XXXX0koYI5hASXZaExeYsGlqz1bSIcGyEg',
'token_type': u'Bearer',
'expires_in': 3599,
'refresh_token': u'XXXXXXe4TlPbNSXQocR4zlxQUdrit5sZ1FutZcece9'
}
def getUserEntry(token):
return {
"sub": getUserId(None)
}
class OpenIdConnectLoginTestCase(ERP5TypeTestCase):
cache_factory = "openid_connect_server_auth_token_cache_factory"
def afterSetUp(self):
"""
This is ran before anything, used to set the environment
"""
self.login()
self.portal.TemplateTool_checkOpenIdConnectExtractionPluginExistenceConsistency(fixit=True)
self.dummy_connector_id = "test_openid_connect_connector"
portal_catalog = self.portal.portal_catalog
for obj in portal_catalog(portal_type=["OpenId Connect Login", "Person"],
reference=getUserId(None),
validation_state="validated"):
obj.getObject().invalidate()
uuid_str = uuid.uuid4().hex
obj.setReference(uuid_str)
obj.setUserId(uuid_str)
for connector in portal_catalog(portal_type="OpenId Connect Connector",
validation_state="validated",
id="NOT %s" % self.dummy_connector_id,
reference="default"):
connector.invalidate()
if getattr(self.portal.portal_web_services, self.dummy_connector_id, None) is None:
connector = self.portal.portal_web_services.newContent(id=self.dummy_connector_id,
portal_type="OpenId Connect Connector",
reference="default",
user_id=CLIENT_ID,
password=SECRET_KEY,
url_string=URL_STRING,
scope=SCOPE,
description=DESCRIPTION,
)
connector.validate()
self.tic()
self.logout()
def setStateInCache(self, state):
self.portal.Base_setBearerToken(state, "12234", self.cache_factory)
class TestOpenIdConnectLogin(OpenIdConnectLoginTestCase):
def test_auth_cookie(self):
state=uuid.uuid4().hex
self.setStateInCache(state)
self.portal.REQUEST.environ['QUERY_STRING'] = "Couscous"
request = self.portal.REQUEST
response = request.RESPONSE
# (the secure flag is only set if we accessed through https)
request.setServerURL('https', 'example.com')
with mock.patch(
'erp5.component.extension.OpenIdConnectLoginUtility.getAccessTokenFromCode',
side_effect=getAccessTokenFromCode,
) as getAccessTokenFromCode_mock, \
mock.patch(
'erp5.component.extension.OpenIdConnectLoginUtility.getUserEntry',
side_effect=getUserEntry
) as getUserEntry_mock:
getAccessTokenFromCode_mock.func_code = getAccessTokenFromCode.func_code
getUserEntry_mock.func_code = getUserEntry.func_code
self.portal.ERP5Site_receiveOpenIdCallback(code=CODE, state=state)
getAccessTokenFromCode_mock.assert_called_once()
getUserEntry_mock.assert_called_once()
ac_cookie, = [v for (k, v) in response.listHeaders() if k.lower() == 'set-cookie' and '__ac_openidconnect_hash=' in v]
self.assertIn('; Secure', ac_cookie)
self.assertIn('; HTTPOnly', ac_cookie)
self.assertIn('; SameSite=Lax', ac_cookie)
def test_existing_user(self):
state=uuid.uuid4().hex
self.setStateInCache(state)
self.portal.REQUEST.environ['QUERY_STRING'] = "Couscous"
self.login()
person = self.portal.person_module.newContent(
portal_type='Person',
)
person.newContent(
portal_type='OpenId Connect Login',
reference=getUserId(None)
).validate()
person.newContent(portal_type='Assignment').open()
self.tic()
self.logout()
request = self.portal.REQUEST
response = request.RESPONSE
with mock.patch(
'erp5.component.extension.OpenIdConnectLoginUtility.getAccessTokenFromCode',
side_effect=getAccessTokenFromCode,
) as getAccessTokenFromCode_mock, \
mock.patch(
'erp5.component.extension.OpenIdConnectLoginUtility.getUserEntry',
side_effect=getUserEntry
) as getUserEntry_mock:
getAccessTokenFromCode_mock.func_code = getAccessTokenFromCode.func_code
getUserEntry_mock.func_code = getUserEntry.func_code
self.portal.ERP5Site_receiveOpenIdCallback(code=CODE, state=state)
getAccessTokenFromCode_mock.assert_called_once()
getUserEntry_mock.assert_called_once()
request["__ac_openidconnect_hash"] = response.cookies["__ac_openidconnect_hash"]["value"]
with mock.patch(
'Products.ERP5Security.ERP5ExternalOauth2ExtractionPlugin._setUserNameForAccessLog'
) as _setUserNameForAccessLog:
credentials = self.portal.acl_users.erp5_openid_connect_extraction.extractCredentials(request)
self.assertEqual(
'OpenId Connect Login',
credentials['login_portal_type'])
self.assertEqual(
getUserId(None),
credentials['external_login'])
# this is what will appear in Z2.log
_setUserNameForAccessLog.assert_called_once_with(
'erp5_openid_connect_extraction=%s' % getUserId(None),
request)
user_id, login = self.portal.acl_users.erp5_login_users.authenticateCredentials(credentials)
self.assertEqual(person.getUserId(), user_id)
self.assertEqual(getUserId(None), login)
self.login(user_id)
self.assertEqual(self.portal.Base_getUserCaption(), login)
def test_logout(self):
resp = self.publish(self.portal.getId() + '/logout')
self.assertEqual(resp.getCookie("__ac_openidconnect_hash")['value'], 'deleted')
def test_create_user_in_ERP5Site_createOpenIdConnectUserToOAuth(self):
"""
Check if ERP5 set cookie properly after receive code from external service
"""
state=uuid.uuid4().hex
self.setStateInCache(state)
self.portal.REQUEST.environ['QUERY_STRING'] = "Couscous"
self.login()
id_list = []
for result in self.portal.portal_catalog(portal_type="Credential Request",
reference=getUserId(None)):
id_list.append(result.getObject().getId())
self.portal.credential_request_module.manage_delObjects(ids=id_list)
skin = self.portal.portal_skins.custom
createZODBPythonScript(skin, "CredentialRequest_createUser", "", """
person = context.getDestinationDecisionValue(portal_type="Person")
login_list = [x for x in person.objectValues(portal_type='OpenId Connect Login') \
if x.getValidationState() == 'validated']
if len(login_list):
login = login_list[0]
else:
login = person.newContent(portal_type='OpenId Connect Login')
reference = context.getReference()
if not login.hasReference():
if not reference:
raise ValueError("Impossible to create an account without login")
login.setReference(reference)
if not person.Person_getUserId():
person.setUserId(reference)
if login.getValidationState() == 'draft':
login.validate()
return reference, None
""")
createZODBPythonScript(skin, "ERP5Site_createOpenIdConnectUserToOAuth", "user_reference, user_dict", """
module = context.getPortalObject().getDefaultModule(portal_type='Credential Request')
credential_request = module.newContent(
portal_type="Credential Request",
first_name=user_dict["sub"],
reference=user_reference,
)
credential_request.submit()
context.portal_alarms.accept_submitted_credentials.activeSense()
return credential_request
""")
self.logout()
with mock.patch(
'erp5.component.extension.OpenIdConnectLoginUtility.getAccessTokenFromCode',
side_effect=getAccessTokenFromCode,
) as getAccessTokenFromCode_mock, \
mock.patch(
'erp5.component.extension.OpenIdConnectLoginUtility.getUserEntry',
side_effect=getUserEntry
) as getUserEntry_mock:
getAccessTokenFromCode_mock.func_code = getAccessTokenFromCode.func_code
getUserEntry_mock.func_code = getUserEntry.func_code
self.portal.ERP5Site_receiveOpenIdCallback(code=CODE, state=state)
getAccessTokenFromCode_mock.assert_called_once()
getUserEntry_mock.assert_called_once()
open_id_connect_hash = self.portal.REQUEST.RESPONSE.cookies.get("__ac_openidconnect_hash")["value"]
self.assertEqual("917b08e860593a6d0530c4cad5758f54", open_id_connect_hash)
absolute_url = self.portal.absolute_url()
self.assertNotEqual(absolute_url[-1], '/')
cache_dict = self.portal.Base_getBearerToken(open_id_connect_hash, "openid_connect_server_auth_token_cache_factory")
self.assertEqual(ACCESS_TOKEN, cache_dict["access_token"])
self.assertEqual({'reference': getUserId(None)},
self.portal.Base_getBearerToken(ACCESS_TOKEN, "openid_connect_server_auth_token_cache_factory")
)
self.portal.REQUEST["__ac_openidconnect_hash"] = open_id_connect_hash
erp5_openid_connect_extraction = self.portal.acl_users.erp5_openid_connect_extraction
self.assertEqual({'external_login': getUserId(None),
'login_portal_type': 'OpenId Connect Login',
'remote_host': '',
'remote_address': ''}, erp5_openid_connect_extraction.extractCredentials(self.portal.REQUEST))
self.tic()
self.login()
credential_request = self.portal.portal_catalog(portal_type="Credential Request",
reference=getUserId(None))[0].getObject()
credential_request.accept()
person = credential_request.getDestinationDecisionValue()
oidc_login = person.objectValues(portal_types="OpenId Connect Login")[0]
self.assertEqual(getUserId(None), oidc_login.getReference())
def test_redirect(self):
"""
Check URL generate to redirect to OpenId Connect
"""
return "EXpected Failure"
self.logout()
self.portal.ERP5Site_redirectToOpenIdLoginPage()
location = self.portal.REQUEST.RESPONSE.getHeader("Location")
self.assertIn(URL_STRING, location)
self.assertIn("response_type=code", location)
self.assertIn("client_id=%s" % CLIENT_ID, location)
self.assertNotIn("secret_key=", location)
self.assertIn("https://testdomain.erp5.net/hateoas/connection/oid_auth", location)
class TestERP5JSOpenIdConnectLogin(OpenIdConnectLoginTestCase):
def _getWebSite(self):
return self.portal.web_site_module.renderjs_runner
def test_login_form(self):
resp = self.publish(self._getWebSite().getPath() + '/login_form')
tree = lxml.etree.fromstring(resp.getBody(), parser=lxml.etree.HTMLParser())
openid_connect_login_link_list = [
link
for link in tree.findall('.//a')
if '/ERP5Site_redirectToOpenIdLoginPage' in link.attrib['href']
]
self.assertEqual(len(openid_connect_login_link_list), 1)
def test_logout(self):
resp = self.publish(self._getWebSite().getPath() + '/WebSite_logout')
self.assertEqual(resp.getCookie("__ac_openidconnect_hash")['value'], 'deleted')
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Test Component" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_recorded_property_dict</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>default_reference</string> </key>
<value> <string>testOpenIdConnectLogin</string> </value>
</item>
<item>
<key> <string>default_source_reference</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>test.erp5.testOpenIdConnectLogin</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Test Component</string> </value>
</item>
<item>
<key> <string>sid</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>text_content_error_message</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>text_content_warning_message</string> </key>
<value>
<tuple>
<string>W:298, 4: Unreachable code (unreachable)</string>
</tuple>
</value>
</item>
<item>
<key> <string>version</string> </key>
<value> <string>erp5</string> </value>
</item>
<item>
<key> <string>workflow_history</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
</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>
<item>
<key> <string>component_validation_workflow</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAQ=</string> </persistent>
</value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="4" aka="AAAAAAAAAAQ=">
<pickle>
<global name="WorkflowHistoryList" module="Products.ERP5Type.Workflow"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_log</string> </key>
<value>
<list>
<dictionary>
<item>
<key> <string>action</string> </key>
<value> <string>validate</string> </value>
</item>
<item>
<key> <string>validation_state</string> </key>
<value> <string>validated</string> </value>
</item>
</dictionary>
</list>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
erp5_bearer_token
erp5_web_service
erp5_oauth
\ No newline at end of file
OpenId Connect Connector | view
OpenId Connect Login | view
\ No newline at end of file
extension.erp5.OpenIdConnectLoginUtility
\ No newline at end of file
portal_caches/openid_connect_server_auth_token_cache_factory
portal_caches/openid_connect_server_auth_token_cache_factory/**
\ No newline at end of file
Person | OpenId Connect Login
Web Service Tool | OpenId Connect Connector
\ No newline at end of file
OpenId Connect Connector
OpenId Connect Login
\ No newline at end of file
OpenId Connect Connector | Login
OpenId Connect Connector | OpenIdConnectConnector
OpenId Connect Connector | Reference
OpenId Connect Connector | Url
Template Tool | TemplateToolERP5OpenIdConnectExtractionPluginConstraint
\ No newline at end of file
OpenId Connect Connector | edit_workflow
OpenId Connect Connector | validation_workflow
OpenId Connect Login | edit_workflow
OpenId Connect Login | login_interaction_workflow
OpenId Connect Login | validation_workflow
\ No newline at end of file
OpenIdConnectConnector
\ No newline at end of file
erp5_openid_connect_client
\ No newline at end of file
test.erp5.testOpenIdConnectLogin
\ No newline at end of file
erp5_full_text_myisam_catalog
erp5_credential
erp5_web_renderjs_ui
\ No newline at end of file
erp5_openid_connect_client_login
\ No newline at end of file
...@@ -22,6 +22,9 @@ if getattr(portal.portal_skins, "erp5_oauth_google_login", None): ...@@ -22,6 +22,9 @@ if getattr(portal.portal_skins, "erp5_oauth_google_login", None):
if getattr(portal.portal_skins, "erp5_oauth_facebook_login", None): if getattr(portal.portal_skins, "erp5_oauth_facebook_login", None):
REQUEST.RESPONSE.expireCookie('__ac_facebook_hash', path='/') REQUEST.RESPONSE.expireCookie('__ac_facebook_hash', path='/')
if getattr(portal.portal_skins, "erp5_openid_connect_client", None):
REQUEST.RESPONSE.expireCookie('__ac_openidconnect_hash', path='/')
# PAS logout, if user is from a PAS user folder (which is the acquisition parent of the user) # PAS logout, if user is from a PAS user folder (which is the acquisition parent of the user)
getattr( getattr(
user, user,
......
...@@ -22,6 +22,9 @@ if getattr(portal.portal_skins, "erp5_oauth_google_login", None): ...@@ -22,6 +22,9 @@ if getattr(portal.portal_skins, "erp5_oauth_google_login", None):
if getattr(portal.portal_skins, "erp5_oauth_facebook_login", None): if getattr(portal.portal_skins, "erp5_oauth_facebook_login", None):
REQUEST.RESPONSE.expireCookie('__ac_facebook_hash', path='/') REQUEST.RESPONSE.expireCookie('__ac_facebook_hash', path='/')
if getattr(portal.portal_skins, "erp5_openid_connect_client", None):
REQUEST.RESPONSE.expireCookie('__ac_openidconnect_hash', path='/')
# PAS logout, if user is from a PAS user folder (which is the acquisition parent of the user) # PAS logout, if user is from a PAS user folder (which is the acquisition parent of the user)
getattr( getattr(
user, user,
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
portal context/getPortalObject; portal context/getPortalObject;
available_oauth_login_list python: portal.ERP5Site_getAvailableOAuthLoginList(); available_oauth_login_list python: portal.ERP5Site_getAvailableOAuthLoginList();
enable_google_login python: 'google' in available_oauth_login_list; enable_google_login python: 'google' in available_oauth_login_list;
enable_openidconnect_login python: 'openidconnect' in available_oauth_login_list;
"> ">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
...@@ -73,6 +74,15 @@ ...@@ -73,6 +74,15 @@
<img alt="Sign in with Google" src="data:image/png;base64,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"/> <img alt="Sign in with Google" src="data:image/png;base64,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"/>
</a> </a>
</div> </div>
<div class="dialog_button_container" tal:condition="enable_openidconnect_login"
tal:define="current_url python: context.getWebSiteValue().absolute_url()">
<a tal:attributes="href string:${current_url}/ERP5Site_redirectToOpenIdLoginPage"
i18n:translate=""
i18n:domain="ui"
>
<img alt="Sign in with OpenID Connect" src="data:image/png;base64,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"/>
</a>
</div>
</div> </div>
</div> </div>
......
...@@ -19,6 +19,9 @@ if getattr(portal.portal_skins, "erp5_oauth_google_login", None): ...@@ -19,6 +19,9 @@ if getattr(portal.portal_skins, "erp5_oauth_google_login", None):
if getattr(portal.portal_skins, "erp5_oauth_facebook_login", None): if getattr(portal.portal_skins, "erp5_oauth_facebook_login", None):
REQUEST.RESPONSE.expireCookie('__ac_facebook_hash', path='/') REQUEST.RESPONSE.expireCookie('__ac_facebook_hash', path='/')
if getattr(portal.portal_skins, "erp5_openid_connect_client", None):
REQUEST.RESPONSE.expireCookie('__ac_openidconnect_hash', path='/')
# PAS logout, if user is from a PAS user folder (which is the acquisition parent of the user) # PAS logout, if user is from a PAS user folder (which is the acquisition parent of the user)
getattr( getattr(
user, user,
......
...@@ -8,5 +8,7 @@ if getattr(portal_skin, "erp5_oauth_google_login", None) is not None: ...@@ -8,5 +8,7 @@ if getattr(portal_skin, "erp5_oauth_google_login", None) is not None:
if getattr(portal_skin, "erp5_oauth_facebook_login", None) is not None: if getattr(portal_skin, "erp5_oauth_facebook_login", None) is not None:
oauth_login_list.append("facebook") oauth_login_list.append("facebook")
if getattr(portal_skin, "erp5_openid_connect_client", None) is not None:
oauth_login_list.append("openidconnect")
return oauth_login_list return oauth_login_list
...@@ -6,7 +6,8 @@ ...@@ -6,7 +6,8 @@
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;
enable_facebook_login python: 'facebook' in available_oauth_login_list; 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 []; enable_openidconnect_login python: 'openidconnect' in available_oauth_login_list;
css_list python: (enable_google_login or enable_facebook_login or enable_openidconnect_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">
...@@ -70,6 +71,15 @@ ...@@ -70,6 +71,15 @@
</div> </div>
</div> </div>
</tal:block> </tal:block>
<tal:block tal:condition="enable_openidconnect_login">
<div class="field">
<label>&nbsp;</label>
<div class="input">
<a tal:attributes="href string:${here/portal_url}/ERP5Site_redirectToOpenIdLoginPage"
i18n:translate="" i18n:domain="ui" class="zocial openid">Login with OpenId Connect</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>
......
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (c) 2012 Nexedi SA and Contributors. All Rights Reserved.
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsibility 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
# guarantees and support are strongly advised 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
##############################################################################
from Products.ERP5Type.Globals import InitializeClass
from AccessControl import ClassSecurityInfo
from Products.PageTemplates.PageTemplateFile import PageTemplateFile
from Products.PluggableAuthService.interfaces import plugins
from Products.PluggableAuthService.utils import classImplements
from Products.PluggableAuthService.plugins.BasePlugin import BasePlugin
from Products.ERP5Security import _setUserNameForAccessLog
from Products.ERP5Security.ERP5ExternalOauth2ExtractionPlugin import ERP5ExternalOauth2ExtractionPlugin
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
#Form for new plugin in ZMI
manage_addERP5OpenIdConnectExtractionPluginForm = PageTemplateFile(
'www/ERP5Security_addERP5OpenIdConnectExtractionPlugin', globals(),
__name__='manage_addERP5OpenIdConnectExtractionPluginForm')
def addERP5OpenIdConnectExtractionPlugin(dispatcher, id, title=None, REQUEST=None):
""" Add a ERP5OpenIdConnectExtractionPlugin to a Pluggable Auth Service. """
plugin = ERP5OpenIdConnectExtractionPlugin(id, title)
dispatcher._setObject(plugin.getId(), plugin)
if REQUEST is not None:
REQUEST['RESPONSE'].redirect(
'%s/manage_workspace'
'?manage_tabs_message='
'ERP5OpenIdConnectExtractionPlugin+added.'
% dispatcher.absolute_url())
class ERP5OpenIdConnectExtractionPlugin(ERP5ExternalOauth2ExtractionPlugin, BasePlugin):
"""
Plugin to authenticate with OpenId Connect.
"""
meta_type = "ERP5 OpenId Connect Extraction Plugin"
login_portal_type = "OpenId Connect Login"
cookie_name = "__ac_openidconnect_hash"
cache_factory_name = "openid_connect_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 = self.portal.ERP5Site_getAccessTokenFromRefreshToken(
response_dict={
'access_token': cache_value["access_token"],
'token_type': cache_value["token_type"],
'expires_in': cache_value["expires_in"],
'refresh_token': cache_value["refresh_token"],
}
)
cache_value = credential
cache_value["response_timestamp"] = time.time()
self.setToken(key, cache_value)
return cache_value
def getUserEntry(self, token):
return self.getPortalObject().ERP5Site_getOpenIdUserEntry(token)
#List implementation of class
classImplements( ERP5OpenIdConnectExtractionPlugin,
plugins.ILoginPasswordHostExtractionPlugin
)
InitializeClass(ERP5OpenIdConnectExtractionPlugin)
\ No newline at end of file
...@@ -85,6 +85,7 @@ def initialize(context): ...@@ -85,6 +85,7 @@ def initialize(context):
ERP5ExternalOauth2ExtractionPlugin, ERP5ExternalOauth2ExtractionPlugin,
ERP5AccessTokenExtractionPlugin, ERP5AccessTokenExtractionPlugin,
ERP5DumbHTTPExtractionPlugin, ERP5DumbHTTPExtractionPlugin,
ERP5ExternalOpenIdConnectExtractionPlugin,
) )
registerMultiPlugin(ERP5UserManager.ERP5UserManager.meta_type) registerMultiPlugin(ERP5UserManager.ERP5UserManager.meta_type)
...@@ -99,6 +100,7 @@ def initialize(context): ...@@ -99,6 +100,7 @@ def initialize(context):
registerMultiPlugin(ERP5ExternalOauth2ExtractionPlugin.ERP5GoogleExtractionPlugin.meta_type) registerMultiPlugin(ERP5ExternalOauth2ExtractionPlugin.ERP5GoogleExtractionPlugin.meta_type)
registerMultiPlugin(ERP5AccessTokenExtractionPlugin.ERP5AccessTokenExtractionPlugin.meta_type) registerMultiPlugin(ERP5AccessTokenExtractionPlugin.ERP5AccessTokenExtractionPlugin.meta_type)
registerMultiPlugin(ERP5DumbHTTPExtractionPlugin.ERP5DumbHTTPExtractionPlugin.meta_type) registerMultiPlugin(ERP5DumbHTTPExtractionPlugin.ERP5DumbHTTPExtractionPlugin.meta_type)
registerMultiPlugin(ERP5ExternalOpenIdConnectExtractionPlugin.ERP5OpenIdConnectExtractionPlugin.meta_type)
context.registerClass( ERP5UserManager.ERP5UserManager context.registerClass( ERP5UserManager.ERP5UserManager
...@@ -209,6 +211,15 @@ def initialize(context): ...@@ -209,6 +211,15 @@ def initialize(context):
, icon='www/portal.gif' , icon='www/portal.gif'
) )
context.registerClass( ERP5ExternalOpenIdConnectExtractionPlugin.ERP5OpenIdConnectExtractionPlugin
, permission=ManageUsers
, constructors=(
ERP5ExternalOpenIdConnectExtractionPlugin.manage_addERP5OpenIdConnectExtractionPluginForm,
ERP5ExternalOpenIdConnectExtractionPlugin.addERP5OpenIdConnectExtractionPlugin, )
, visibility=None
, icon='www/portal.gif'
)
from AccessControl.SecurityInfo import ModuleSecurityInfo from AccessControl.SecurityInfo import ModuleSecurityInfo
ModuleSecurityInfo('Products.ERP5Security.ERP5UserManager').declarePublic( ModuleSecurityInfo('Products.ERP5Security.ERP5UserManager').declarePublic(
'getUserByLogin') 'getUserByLogin')
...@@ -781,6 +781,14 @@ class TestPASAPI(UserManagementTestCase): ...@@ -781,6 +781,14 @@ class TestPASAPI(UserManagementTestCase):
ERP5BearerExtractionPlugin ERP5BearerExtractionPlugin
verifyClass(ILoginPasswordHostExtractionPlugin, ERP5BearerExtractionPlugin) verifyClass(ILoginPasswordHostExtractionPlugin, ERP5BearerExtractionPlugin)
def test_ERP5OpenIdConnectExtractionPluginInterfaces(self):
"""Tests openid connect extraction plugin respects interfaces."""
from Products.PluggableAuthService.interfaces.plugins import\
ILoginPasswordHostExtractionPlugin
from Products.ERP5Security.ERP5ExternalOpenIdConnectExtractionPlugin import\
ERP5OpenIdConnectExtractionPlugin
verifyClass(ILoginPasswordHostExtractionPlugin, ERP5OpenIdConnectExtractionPlugin)
def test_ERP5DumbHTTPExtractionPluginInterfaces(self): def test_ERP5DumbHTTPExtractionPluginInterfaces(self):
"""Tests dumb HTTP extraction plugin respects interfaces.""" """Tests dumb HTTP extraction plugin respects interfaces."""
from Products.PluggableAuthService.interfaces.plugins import\ from Products.PluggableAuthService.interfaces.plugins import\
......
<h1 tal:replace="structure context/manage_page_header">PAGE HEADER</h1>
<h2 tal:define="form_title string:Add ERP5 OpenId Connect Extraction Plugin"
tal:replace="structure context/manage_form_title">FORM TITLE</h2>
<p class="form-help">Please input the configuration</p>
<form action="addERP5OpenIdConnectExtractionPlugin" method="POST">
<table cellspacing="0" cellpadding="2" border="0">
<tr>
<td align="left" valign="top">
<div class="form-label">
Id
</div>
</td>
<td align="left" valign="top">
<input type="text" name="id" size="40" />
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
Title
</div>
</td>
<td align="left" valign="top">
<input type="text" name="title" size="40" />
</td>
</tr>
<tr>
<td colspan="2"> <input type="submit" value="add plugin"/>
</td>
</tr>
</table>
</form>
<h1 tal:replace="structure context/manage_page_footer">PAGE FOOTER</h1>
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