Commit e9d8d22f authored by Titouan Soulard's avatar Titouan Soulard

erp5_oauth2_authorisation: allow creating single-use refresh tokens

By default, refresh tokens generated using OAuth2 can be used as long as they
are valid. This commit introduces an option, which, when enabled, makes it
impossible to use already-used refresh tokens.
parent 31cd55e4
...@@ -90,6 +90,7 @@ from Products.ERP5Security.ERP5OAuth2ResourceServerPlugin import ( ...@@ -90,6 +90,7 @@ from Products.ERP5Security.ERP5OAuth2ResourceServerPlugin import (
JWT_PAYLOAD_ROLE_LIST_KEY, JWT_PAYLOAD_ROLE_LIST_KEY,
JWT_PAYLOAD_GROUP_LIST_KEY, JWT_PAYLOAD_GROUP_LIST_KEY,
JWT_PAYLOAD_SCOPE_LIST_KEY, JWT_PAYLOAD_SCOPE_LIST_KEY,
JWT_PAYLOAD_REFRESH_TOKEN_REVISION_KEY,
JWT_CLAIM_NETWORK_LIST_KEY, JWT_CLAIM_NETWORK_LIST_KEY,
) )
from ZPublisher.HTTPResponse import HTTPResponse from ZPublisher.HTTPResponse import HTTPResponse
...@@ -782,9 +783,20 @@ class _ERP5RequestValidator(RequestValidator): ...@@ -782,9 +783,20 @@ class _ERP5RequestValidator(RequestValidator):
if session_value is not None: if session_value is not None:
with super_user(): with super_user():
session_client_id = session_value.getSourceId() session_client_id = session_value.getSourceId()
session_refresh_token_revision = session_value.getRefreshTokenRevision()
if client.erp5_client_value.isRefreshTokenSingleUse():
refresh_token_revision = self._authorisation_server_connector_value.getRevisionFromRefreshToken(
token=refresh_token,
request=request,
)
if session_refresh_token_revision != refresh_token_revision:
return False
if session_client_id == client.client_id: if session_client_id == client.client_id:
request.user = _SessionUser(session_value) request.user = _SessionUser(session_value)
return True return True
return False return False
@staticmethod @staticmethod
...@@ -1183,15 +1195,26 @@ class OAuth2AuthorisationServerConnector(XMLObject): ...@@ -1183,15 +1195,26 @@ class OAuth2AuthorisationServerConnector(XMLObject):
): ):
session_value.setRefreshTokenExpirationDate(DateTime(expiration_timestamp)) session_value.setRefreshTokenExpirationDate(DateTime(expiration_timestamp))
_, algorithm, symetric_key = self.__getRefreshTokenKeyList()[0] _, algorithm, symetric_key = self.__getRefreshTokenKeyList()[0]
jwt_payload = {
JWT_PAYLOAD_AUTHORISATION_SESSION_ID_KEY: session_value.getId(),
}
if request.client.erp5_client_value.isRefreshTokenSingleUse():
refresh_token_revision = session_value.getRefreshTokenRevision()
if refresh_token_revision is None:
refresh_token_revision = 1
else:
refresh_token_revision += 1
session_value.setRefreshTokenRevision(refresh_token_revision)
jwt_payload[JWT_PAYLOAD_REFRESH_TOKEN_REVISION_KEY] = refresh_token_revision
return jwt.encode( return jwt.encode(
{ {
'exp': expiration_timestamp, 'exp': expiration_timestamp,
'iss': request.client.client_id, 'iss': request.client.client_id,
'iat': now, 'iat': now,
JWT_CLAIM_NETWORK_LIST_KEY: session_value.getNetworkList(), JWT_CLAIM_NETWORK_LIST_KEY: session_value.getNetworkList(),
JWT_PAYLOAD_KEY: { JWT_PAYLOAD_KEY: jwt_payload,
JWT_PAYLOAD_AUTHORISATION_SESSION_ID_KEY: session_value.getId(),
},
}, },
key=symetric_key, key=symetric_key,
algorithm=algorithm, algorithm=algorithm,
...@@ -1459,6 +1482,17 @@ class OAuth2AuthorisationServerConnector(XMLObject): ...@@ -1459,6 +1482,17 @@ class OAuth2AuthorisationServerConnector(XMLObject):
if source_id == token_dict['iss']: if source_id == token_dict['iss']:
return session_value return session_value
security.declarePrivate('getRevisionFromRefreshToken')
def getRevisionFromRefreshToken(self, token, request):
"""
Does not check access permission.
"""
try:
token_dict = self._getRefreshTokenDict(token, request)
except jwt.InvalidTokenError:
return
return token_dict[JWT_PAYLOAD_KEY][JWT_PAYLOAD_REFRESH_TOKEN_REVISION_KEY]
security.declarePrivate('getSessionValueFromAccessToken') security.declarePrivate('getSessionValueFromAccessToken')
def getSessionValueFromAccessToken(self, token, request): def getSessionValueFromAccessToken(self, token, request):
""" """
...@@ -1487,8 +1521,12 @@ class OAuth2AuthorisationServerConnector(XMLObject): ...@@ -1487,8 +1521,12 @@ class OAuth2AuthorisationServerConnector(XMLObject):
refresh_token_dict = self._getRefreshTokenDict(refresh_token, request) refresh_token_dict = self._getRefreshTokenDict(refresh_token, request)
except jwt.InvalidTokenError: except jwt.InvalidTokenError:
return False return False
# XXX: allow refresh if exp - iat != getRefreshTokenLifespan (within some imprecision) ? client_value = self[refresh_token_dict['iss']]
return time() - refresh_token_dict['iat'] > self[refresh_token_dict['iss']].getRefreshTokenLifespanAccuracy() return (
client_value.isRefreshTokenSingleUse() or
# XXX: allow refresh if exp - iat != getRefreshTokenLifespan (within some imprecision) ?
time() - refresh_token_dict['iat'] > client_value.getRefreshTokenLifespanAccuracy()
)
# #
# Non-oauth2 methods, for use by ERP5 in the role of a resource server # Non-oauth2 methods, for use by ERP5 in the role of a resource server
......
<?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/boolean</string>
</tuple>
</value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>refresh_token_single_use_property</string> </value>
</item>
</dictionary>
</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/int</string>
</tuple>
</value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>refresh_token_revision_property</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
...@@ -80,6 +80,7 @@ ...@@ -80,6 +80,7 @@
<string>my_redirect_uri_list</string> <string>my_redirect_uri_list</string>
<string>my_network_list</string> <string>my_network_list</string>
<string>my_access_token_lifespan</string> <string>my_access_token_lifespan</string>
<string>my_refresh_token_single_use</string>
<string>my_refresh_token_lifespan</string> <string>my_refresh_token_lifespan</string>
<string>my_refresh_token_lifespan_accuracy</string> <string>my_refresh_token_lifespan_accuracy</string>
<string>my_oauth2_scope_list</string> <string>my_oauth2_scope_list</string>
......
<?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_refresh_token_single_use</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_checkbox</string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string>Base_viewFieldLibrary</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Refresh Token Single-use</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
...@@ -97,6 +97,7 @@ ...@@ -97,6 +97,7 @@
<string>my_int_index</string> <string>my_int_index</string>
<string>my_network_list</string> <string>my_network_list</string>
<string>my_policy_expiration_date</string> <string>my_policy_expiration_date</string>
<string>my_refresh_token_revision</string>
<string>my_refresh_token_expiration_date</string> <string>my_refresh_token_expiration_date</string>
<string>my_expiration_date</string> <string>my_expiration_date</string>
<string>my_translated_validation_state_title</string> <string>my_translated_validation_state_title</string>
......
<?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>editable</string>
<string>title</string>
</list>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>my_refresh_token_revision</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>editable</string> </key>
<value> <int>0</int> </value>
</item>
<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>title</string> </key>
<value> <string>Refresh Token Revision</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
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