diff --git a/analytical/templatetags/intercom.py b/analytical/templatetags/intercom.py
index f0f50d1..888a0ab 100644
--- a/analytical/templatetags/intercom.py
+++ b/analytical/templatetags/intercom.py
@@ -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):
diff --git a/analytical/tests/test_tag_intercom.py b/analytical/tests/test_tag_intercom.py
index bc24739..2085fcb 100644
--- a/analytical/tests/test_tag_intercom.py
+++ b/analytical/tests/test_tag_intercom.py
@@ -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("""
-""", 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()
diff --git a/docs/services/intercom.rst b/docs/services/intercom.rst
index 700fc1e..f458d28 100644
--- a/docs/services/intercom.rst
+++ b/docs/services/intercom.rst
@@ -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
---------------------