diff --git a/analytical/templatetags/intercom.py b/analytical/templatetags/intercom.py index 27e9047..92badec 100644 --- a/analytical/templatetags/intercom.py +++ b/analytical/templatetags/intercom.py @@ -3,10 +3,14 @@ intercom.io template tags and filters. """ from __future__ import absolute_import + +import hashlib +import hmac import json 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 +28,34 @@ TRACKING_CODE = """ register = Library() +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): """ @@ -73,6 +105,16 @@ class IntercomNode(Node): 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 754081c..03cc47f 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 from analytical.tests.utils import TagTestCase from analytical.utils import AnalyticalException @@ -103,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(user.date_joined.timestamp()), + '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 5c9f250..f458d28 100644 --- a/docs/services/intercom.rst +++ b/docs/services/intercom.rst @@ -138,6 +138,23 @@ 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 ---------------------