Commit 47cf2cca authored by Laurent Wouters's avatar Laurent Wouters
Browse files

Support alternative to Date header

The current protocol for signed request requires the use of the Date HTTP
header. Although this works fine for clients that have full control over the
HTTP headers they send, this is not working in the context of web browser where
the Date HTTP headers are forbidden to be programmatically set (and therefore
used in any meaningful way)
https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_header_name

In general, this change enables the specification of a prioritized list of
alternative for headers. In particular for the Date header, this change
specifies a the list ['X-Cubicweb-Date', 'Date'] as an alternative to the Date
header; meaning that when looking for the Date header, one should first look
at the X-Cubicweb-Date header, and then if not present at the Date header. Doing
so, it should be possible to emit signed requests from the context of a browser
by specifying a X-Cubicweb-Date header, overriding the Date header that the
browser may or may not set by itself.
parent 9cf3d9ab91e3
......@@ -48,6 +48,29 @@ except ImportError:
log = logging.getLogger(__name__)
HEADERS_TO_SIGN = ('Content-MD5', 'Content-Type', 'Date')
ALTERNATE_HEADERS = {'Date': ['X-Cubicweb-Date', 'Date']}
def get_replaceable_header_value(request, header_name, default=None):
"""
Get the value for a header, looking at prioritized alternatives as required
:param request: The request
:param header_name: The name of the header
:param default: The default value in case the header is not specified
:return: The value of the header, or its alternatives
"""
try:
alternates = ALTERNATE_HEADERS[header_name]
# we have a list of prioritized headers
for alt_header_name in alternates:
value = request.get_header(alt_header_name)
if value is not None:
return value
return default
except KeyError:
# default behavior
value = request.get_header(header_name)
return value or default
def hash_content(content):
......@@ -73,7 +96,7 @@ def get_credentials_from_headers(request, content_md5):
forged using the token's secret key to authenticate
the user linked with the AuthToken
"""
header = request.get_header('Authorization', None)
header = get_replaceable_header_value(request, 'Authorization', None)
if header is None:
log.debug('SIGNED REQUEST: error header is none')
return
......@@ -87,12 +110,12 @@ def get_credentials_from_headers(request, content_md5):
log.debug('SIGNED REQUEST: method is not Cubicweb')
return
if request.http_method() != 'GET':
if content_md5 != request.get_header('Content-MD5'):
if content_md5 != get_replaceable_header_value(request, 'Content-MD5'):
log.error('SIGNED REQUEST: wrong md5, %s != %s' % (
content_md5,
request.get_header('Content-MD5')))
get_replaceable_header_value(request, 'Content-MD5')))
raise AuthenticationError()
date_header = request.get_header('Date')
date_header = get_replaceable_header_value(request, 'Date')
if date_header is None:
raise AuthenticationError()
try:
......@@ -125,7 +148,7 @@ def build_string_to_sign(request, url=None, headers=None):
headers = HEADERS_TO_SIGN
if url is None:
url = request.url
get_header = lambda field: request.get_header(field, '') # noqa
get_header = lambda field: get_replaceable_header_value(request, field, '') # noqa
return (request.http_method() + url +
''.join(map(get_header, headers))).encode('utf-8')
......
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