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 (
JWT_PAYLOAD_ROLE_LIST_KEY,
JWT_PAYLOAD_GROUP_LIST_KEY,
JWT_PAYLOAD_SCOPE_LIST_KEY,
JWT_PAYLOAD_REFRESH_TOKEN_REVISION_KEY,
JWT_CLAIM_NETWORK_LIST_KEY,
)
from ZPublisher.HTTPResponse import HTTPResponse
......@@ -782,9 +783,20 @@ class _ERP5RequestValidator(RequestValidator):
if session_value is not None:
with super_user():
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:
request.user = _SessionUser(session_value)
return True
return False
@staticmethod
......@@ -1183,15 +1195,26 @@ class OAuth2AuthorisationServerConnector(XMLObject):
):
session_value.setRefreshTokenExpirationDate(DateTime(expiration_timestamp))
_, 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(
{
'exp': expiration_timestamp,
'iss': request.client.client_id,
'iat': now,
JWT_CLAIM_NETWORK_LIST_KEY: session_value.getNetworkList(),
JWT_PAYLOAD_KEY: {
JWT_PAYLOAD_AUTHORISATION_SESSION_ID_KEY: session_value.getId(),
},
JWT_PAYLOAD_KEY: jwt_payload,
},
key=symetric_key,
algorithm=algorithm,
......@@ -1459,6 +1482,17 @@ class OAuth2AuthorisationServerConnector(XMLObject):
if source_id == token_dict['iss']:
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')
def getSessionValueFromAccessToken(self, token, request):
"""
......@@ -1487,8 +1521,12 @@ class OAuth2AuthorisationServerConnector(XMLObject):
refresh_token_dict = self._getRefreshTokenDict(refresh_token, request)
except jwt.InvalidTokenError:
return False
# XXX: allow refresh if exp - iat != getRefreshTokenLifespan (within some imprecision) ?
return time() - refresh_token_dict['iat'] > self[refresh_token_dict['iss']].getRefreshTokenLifespanAccuracy()
client_value = self[refresh_token_dict['iss']]
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
......
<?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 @@
<string>my_redirect_uri_list</string>
<string>my_network_list</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_accuracy</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 @@
<string>my_int_index</string>
<string>my_network_list</string>
<string>my_policy_expiration_date</string>
<string>my_refresh_token_revision</string>
<string>my_refresh_token_expiration_date</string>
<string>my_expiration_date</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