Commit 9c4d0edd authored by Cédric Le Ninivin's avatar Cédric Le Ninivin

Introduce OpenId Connect

erp5_openid_connect_client_login: Add dedicated cache factory to work with open id connect

erp5_openid_connect_client_login: Add Scope, Client Metadata and state

* Add configurable scope to connector
* Client Metadata is a JSON defined on connector
* Add state parameter to make redirect non repeatable

erp5_openid_connect_client_login: Update OpenId Connect to be be fully functionnal

erp5_openid_connect_client_login: Add tests and some minor fixup

erp5_openid_connect_client_login: Don't call create user on each call to callback

erp5_openid_connect_client_login: Test create connector in portal web services

erp5_openid_connect_client_login: Fix test on open Id connector creation in afterSetUp

OpenIDConnect: Add Extraction Plugin

openidconnect: Have functionnal Extractor

erp5_core: Add Case of OpenId Connect Client

erp5_xhtml_style: Add OpenId Connect to Login Form

erp5_web: Add OpenId Connect Logout

erp5_web_renderjs_ui: Add OpenId Connect to Login Form and Logout

erp5_credential: Add OpenId Connect to login form
parent 9fbfeaac
......@@ -6,6 +6,7 @@
available_oauth_login_list python: context.getPortalObject().ERP5Site_getAvailableOAuthLoginList();
enable_google_login python: 'google' 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 [];
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">
......@@ -71,6 +72,15 @@
</div>
</div>
</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>
<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>
......
<?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>