Merge pull request #134 from pjdelport/intercom-hmac-identity-verification

Support Intercom HMAC identity verification
This commit is contained in:
Joost Cassee 2018-11-15 20:10:39 +01:00 committed by GitHub
commit d08da39fb1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 155 additions and 22 deletions

View file

@ -3,10 +3,15 @@ intercom.io template tags and filters.
"""
from __future__ import absolute_import
import hashlib
import hmac
import json
import sys
import time
import re
from django.conf import settings
from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import disable_html, get_required_setting, \
@ -24,6 +29,42 @@ TRACKING_CODE = """
register = Library()
def _timestamp(when): # type: (datetime) -> float
"""
Python 2 compatibility for `datetime.timestamp()`.
"""
return (time.mktime(when.timetuple()) if sys.version_info < (3,) else
when.timestamp())
def _hashable_bytes(data): # type: (AnyStr) -> bytes
"""
Coerce strings to hashable bytes.
"""
if isinstance(data, bytes):
return data
elif isinstance(data, str):
return data.encode('ascii') # Fail on anything non-ASCII.
else:
raise TypeError(data)
def intercom_user_hash(data): # type: (AnyStr) -> Optional[str]
"""
Return a SHA-256 HMAC `user_hash` as expected by Intercom, if configured.
Return None if the `INTERCOM_HMAC_SECRET_KEY` setting is not configured.
"""
if getattr(settings, 'INTERCOM_HMAC_SECRET_KEY', None):
return hmac.new(
key=_hashable_bytes(settings.INTERCOM_HMAC_SECRET_KEY),
msg=_hashable_bytes(data),
digestmod=hashlib.sha256,
).hexdigest()
else:
return None
@register.tag
def intercom(parser, token):
"""
@ -66,11 +107,22 @@ class IntercomNode(Node):
if 'email' not in params and user.email:
params['email'] = user.email
params['created_at'] = int(time.mktime(
user.date_joined.timetuple()))
params.setdefault('user_id', user.pk)
params['created_at'] = int(_timestamp(user.date_joined))
else:
params['created_at'] = None
# Generate a user_hash HMAC to verify the user's identity, if configured.
# (If both user_id and email are present, the user_id field takes precedence.)
# See:
# https://www.intercom.com/help/configure-intercom-for-your-product-or-site/staying-secure/enable-identity-verification-on-your-web-product
user_hash_data = params.get('user_id', params.get('email')) # type: Optional[str]
if user_hash_data:
user_hash = intercom_user_hash(str(user_hash_data)) # type: Optional[str]
if user_hash is not None:
params.setdefault('user_hash', user_hash)
return params
def render(self, context):

View file

@ -9,7 +9,7 @@ from django.http import HttpRequest
from django.template import Context
from django.test.utils import override_settings
from analytical.templatetags.intercom import IntercomNode
from analytical.templatetags.intercom import IntercomNode, intercom_user_hash, _timestamp
from analytical.tests.utils import TagTestCase
from analytical.utils import AnalyticalException
@ -26,21 +26,21 @@ class IntercomTagTestCase(TagTestCase):
def test_node(self):
now = datetime.datetime(2014, 4, 9, 15, 15, 0)
rendered_tag = IntercomNode().render(Context({
'user': User(
username='test',
first_name='Firstname',
last_name='Lastname',
email="test@example.com",
date_joined=now),
}))
user = User.objects.create(
username='test',
first_name='Firstname',
last_name='Lastname',
email="test@example.com",
date_joined=now,
)
rendered_tag = IntercomNode().render(Context({'user': user}))
# Because the json isn't predictably ordered, we can't just test the whole thing verbatim.
self.assertEqual("""
<script id="IntercomSettingsScriptTag">
window.intercomSettings = {"app_id": "abc123xyz", "created_at": 1397074500, "email": "test@example.com", "name": "Firstname Lastname"};
window.intercomSettings = {"app_id": "abc123xyz", "created_at": 1397074500, "email": "test@example.com", "name": "Firstname Lastname", "user_id": %(user_id)s};
</script>
<script>(function(){var w=window;var ic=w.Intercom;if(typeof ic==="function"){ic('reattach_activator');ic('update',intercomSettings);}else{var d=document;var i=function(){i.c(arguments)};i.q=[];i.c=function(args){i.q.push(args)};w.Intercom=i;function l(){var s=d.createElement('script');s.type='text/javascript';s.async=true;s.src='https://static.intercomcdn.com/intercom.v1.js';var x=d.getElementsByTagName('script')[0];x.parentNode.insertBefore(s,x);}if(w.attachEvent){w.attachEvent('onload',l);}else{w.addEventListener('load',l,false);}}})()</script>
""", rendered_tag) # noqa
""" % {'user_id': user.pk}, rendered_tag) # noqa
@override_settings(INTERCOM_APP_ID=None)
def test_no_account_number(self):
@ -52,18 +52,21 @@ class IntercomTagTestCase(TagTestCase):
def test_identify_name_email_and_created_at(self):
now = datetime.datetime(2014, 4, 9, 15, 15, 0)
user = User.objects.create(
username='test',
first_name='Firstname',
last_name='Lastname',
email="test@example.com",
date_joined=now,
)
r = IntercomNode().render(Context({
'user': User(
username='test',
first_name='Firstname',
last_name='Lastname',
email="test@example.com",
date_joined=now),
'user': user,
}))
self.assertTrue('window.intercomSettings = {'
'"app_id": "abc123xyz", "created_at": 1397074500, '
'"email": "test@example.com", "name": "Firstname Lastname"'
'};' in r)
'"email": "test@example.com", "name": "Firstname Lastname", '
'"user_id": %(user_id)s'
'};' % {'user_id': user.pk} in r, msg=r)
def test_custom(self):
r = IntercomNode().render(Context({
@ -100,6 +103,65 @@ class IntercomTagTestCase(TagTestCase):
}))
self.assertTrue('"email": "explicit"' in r, r)
@override_settings(INTERCOM_HMAC_SECRET_KEY='secret')
def test_user_hash__without_user_details(self):
"""
No `user_hash` without `user_id` or `email`.
"""
attrs = IntercomNode()._get_custom_attrs(Context())
self.assertEqual({
'created_at': None,
}, attrs)
@override_settings(INTERCOM_HMAC_SECRET_KEY='secret')
def test_user_hash__with_user(self):
"""
'user_hash' of default `user_id`.
"""
user = User.objects.create(
email='test@example.com',
) # type: User
attrs = IntercomNode()._get_custom_attrs(Context({'user': user}))
self.assertEqual({
'created_at': int(_timestamp(user.date_joined)),
'email': 'test@example.com',
'name': '',
'user_hash': intercom_user_hash(str(user.pk)),
'user_id': user.pk,
}, attrs)
@override_settings(INTERCOM_HMAC_SECRET_KEY='secret')
def test_user_hash__with_explicit_user_id(self):
"""
'user_hash' of context-provided `user_id`.
"""
attrs = IntercomNode()._get_custom_attrs(Context({
'intercom_email': 'test@example.com',
'intercom_user_id': '5',
}))
self.assertEqual({
'created_at': None,
'email': 'test@example.com',
# HMAC for user_id:
'user_hash': 'd3123a7052b42272d9b520235008c248a5aff3221cc0c530b754702ad91ab102',
'user_id': '5',
}, attrs)
@override_settings(INTERCOM_HMAC_SECRET_KEY='secret')
def test_user_hash__with_explicit_email(self):
"""
'user_hash' of context-provided `email`.
"""
attrs = IntercomNode()._get_custom_attrs(Context({
'intercom_email': 'test@example.com',
}))
self.assertEqual({
'created_at': None,
'email': 'test@example.com',
# HMAC for email:
'user_hash': '49e43229ee99dca2565241719b8341b04e71dd4de0628f991b5bea30a526e153',
}, attrs)
@override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1'])
def test_render_internal_ip(self):
req = HttpRequest()

View file

@ -120,6 +120,8 @@ Context variable Description
-------------------- -------------------------------------------
``intercom_email`` The visitor's email address.
-------------------- -------------------------------------------
``intercom_user_id`` The visitor's user id.
-------------------- -------------------------------------------
``created_at`` The date the visitor created an account
==================== ===========================================
@ -130,12 +132,29 @@ Context variable Description
Identifying authenticated users
-------------------------------
If you have not set the ``intercom_name`` or ``intercom_email`` variables
If you have not set the ``intercom_name``, ``intercom_email``, or ``intercom_user_id`` variables
explicitly, the username and email address of an authenticated user are
passed to Intercom automatically. See :ref:`identifying-visitors`.
.. _intercom-internal-ips:
Verifying identified users
--------------------------
Intercom supports HMAC authentication of users identified by user ID or email, in order to prevent impersonation.
For more information, see `Enable identity verification on your web product`_ in the Intercom documentation.
To enable this, configure your Intercom account's HMAC secret key::
INTERCOM_HMAC_SECRET_KEY = 'XXXXXXXXXXXXXXXXXXXXXXX'
(You can find this secret key under the "Identity verification" section of your Intercom account settings page.)
.. _`Enable identity verification on your web product`: https://www.intercom.com/help/configure-intercom-for-your-product-or-site/staying-secure/enable-identity-verification-on-your-web-product
Internal IP addresses
---------------------