Skip to content

Commit

Permalink
Merge pull request #134 from pjdelport/intercom-hmac-identity-verific…
Browse files Browse the repository at this point in the history
…ation

Support Intercom HMAC identity verification
  • Loading branch information
jcassee committed Nov 15, 2018
2 parents 2d8cbd2 + 1b7429c commit d08da39
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 22 deletions.
56 changes: 54 additions & 2 deletions analytical/templatetags/intercom.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@
"""

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, \
Expand All @@ -24,6 +29,42 @@
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):
"""
Expand Down Expand Up @@ -66,11 +107,22 @@ def _get_custom_attrs(self, context):
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):
Expand Down
100 changes: 81 additions & 19 deletions analytical/tests/test_tag_intercom.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
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

Expand All @@ -26,21 +26,21 @@ def test_tag(self):

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):
Expand All @@ -52,18 +52,21 @@ def test_wrong_account_number(self):

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({
Expand Down Expand Up @@ -100,6 +103,65 @@ def test_no_identify_when_explicit_email(self):
}))
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()
Expand Down
21 changes: 20 additions & 1 deletion docs/services/intercom.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
==================== ===========================================

Expand All @@ -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
---------------------

Expand Down

0 comments on commit d08da39

Please sign in to comment.