mirror of
https://github.com/jazzband/django-analytical.git
synced 2026-04-02 22:30:31 +00:00
Merge pull request #134 from pjdelport/intercom-hmac-identity-verification
Support Intercom HMAC identity verification
This commit is contained in:
commit
d08da39fb1
3 changed files with 155 additions and 22 deletions
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
---------------------
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue