Skip to content

Commit 0404f08

Browse files
authored
Support experimental JWT feature (not available yet) (#356)
* Fix dev dependencies * Support experimental JWT authentication for the messenger * Remove user_id from regular user_data payload when using JWT * Add some script tag helper specs too * bump version
1 parent 3296abf commit 0404f08

File tree

8 files changed

+169
-4
lines changed

8 files changed

+169
-4
lines changed

intercom-rails.gemspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Gem::Specification.new do |s|
1919
s.test_files = Dir["test/**/*"]
2020

2121
s.add_dependency 'activesupport', '>4.0'
22+
s.add_dependency 'jwt', '~> 2.0'
2223

2324
s.add_development_dependency 'rake'
2425
s.add_development_dependency 'actionpack', '>5.0'

lib/intercom-rails/config.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ def self.reset!
110110
config_accessor :hide_default_launcher
111111
config_accessor :api_base
112112
config_accessor :encrypted_mode
113+
config_accessor :jwt_enabled
113114

114115
def self.api_key=(*)
115116
warn "Setting an Intercom API key is no longer supported; remove the `config.api_key = ...` line from config/initializers/intercom.rb"

lib/intercom-rails/script_tag.rb

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
require 'active_support/all'
44
require 'action_view'
5+
require 'jwt'
56

67
module IntercomRails
78

@@ -17,15 +18,16 @@ class ScriptTag
1718
include ::ActionView::Helpers::TagHelper
1819

1920
attr_reader :user_details, :company_details, :show_everywhere, :session_duration
20-
attr_accessor :secret, :widget_options, :controller, :nonce, :encrypted_mode_enabled, :encrypted_mode
21+
attr_accessor :secret, :widget_options, :controller, :nonce, :encrypted_mode_enabled, :encrypted_mode, :jwt_enabled
2122

2223
def initialize(options = {})
2324
self.secret = options[:secret] || Config.api_secret
2425
self.widget_options = widget_options_from_config.merge(options[:widget] || {})
2526
self.controller = options[:controller]
2627
@show_everywhere = options[:show_everywhere]
2728
@session_duration = session_duration_from_config
28-
29+
self.jwt_enabled = options[:jwt_enabled] || Config.jwt_enabled
30+
2931
initial_user_details = if options[:find_current_user_details]
3032
find_current_user_details
3133
else
@@ -119,12 +121,30 @@ def intercom_javascript
119121
"window.intercomSettings = #{plaintext_javascript};#{intercom_encrypted_payload_javascript}(function(){var w=window;var ic=w.Intercom;if(typeof ic===\"function\"){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='#{Config.library_url || "https://widget.intercom.io/widget/#{j app_id}"}';var x=d.getElementsByTagName('script')[0];x.parentNode.insertBefore(s,x);}if(document.readyState==='complete'){l();}else if(w.attachEvent){w.attachEvent('onload',l);}else{w.addEventListener('load',l,false);}};})()"
120122
end
121123

124+
def generate_jwt
125+
return nil unless user_details[:user_id].present?
126+
127+
payload = {
128+
user_id: user_details[:user_id].to_s,
129+
exp: 24.hours.from_now.to_i
130+
}
131+
JWT.encode(payload, secret, 'HS256')
132+
end
133+
122134
def user_details=(user_details)
123135
@user_details = DateHelper.convert_dates_to_unix_timestamps(user_details || {})
124136
@user_details = @user_details.with_indifferent_access.tap do |u|
125137
[:email, :name, :user_id].each { |k| u.delete(k) if u[k].nil? }
126138

127-
u[:user_hash] ||= user_hash if secret.present? && (u[:user_id] || u[:email]).present?
139+
if secret.present?
140+
if jwt_enabled && u[:user_id].present?
141+
u[:intercom_user_jwt] ||= generate_jwt
142+
u.delete(:user_id) # No need to send plaintext user_id when using JWT
143+
elsif (u[:user_id] || u[:email]).present?
144+
u[:user_hash] ||= user_hash
145+
end
146+
end
147+
128148
u[:app_id] ||= app_id
129149
end
130150
end

lib/intercom-rails/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
module IntercomRails
2-
VERSION = "1.0.2"
2+
VERSION = "1.0.3"
33
end

spec/config_spec.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,4 +114,21 @@
114114
IntercomRails.config.user.company_association = Proc.new { [] }
115115
end.to output(/no longer supported/).to_stderr
116116
end
117+
118+
it 'gets/sets jwt_enabled' do
119+
IntercomRails.config.jwt_enabled = true
120+
expect(IntercomRails.config.jwt_enabled).to eq(true)
121+
end
122+
123+
it 'defaults jwt_enabled to nil' do
124+
IntercomRails.config.reset!
125+
expect(IntercomRails.config.jwt_enabled).to eq(nil)
126+
end
127+
128+
it 'allows jwt_enabled in block form' do
129+
IntercomRails.config do |config|
130+
config.jwt_enabled = true
131+
end
132+
expect(IntercomRails.config.jwt_enabled).to eq(true)
133+
end
117134
end

spec/script_tag_helper_spec.rb

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,35 @@
4949
expect(script_tag.to_s).to include('nonce="pJwtLVnwiMaPCxpb41KZguOcC5mGUYD+8RNGcJSlR94="')
5050
end
5151
end
52+
53+
context 'JWT authentication' do
54+
before(:each) do
55+
allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new("test"))
56+
end
57+
before(:each) do
58+
IntercomRails.config.api_secret = 'super-secret'
59+
end
60+
61+
it 'enables JWT when configured' do
62+
IntercomRails.config.jwt_enabled = true
63+
output = intercom_script_tag({
64+
user_id: '1234',
65+
66+
}).to_s
67+
68+
expect(output).to include('intercom_user_jwt')
69+
expect(output).not_to include('user_hash')
70+
end
71+
72+
it 'falls back to user_hash when JWT is disabled' do
73+
IntercomRails.config.jwt_enabled = false
74+
output = intercom_script_tag({
75+
user_id: '1234',
76+
77+
}).to_s
78+
79+
expect(output).not_to include('intercom_user_jwt')
80+
expect(output).to include('user_hash')
81+
end
82+
end
5283
end

spec/script_tag_spec.rb

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
require 'active_support/time'
22
require 'spec_helper'
3+
require 'jwt'
34

45
describe IntercomRails::ScriptTag do
56
ScriptTag = IntercomRails::ScriptTag
@@ -301,4 +302,97 @@ def user
301302
end
302303
end
303304

305+
context 'JWT authentication' do
306+
before(:each) do
307+
IntercomRails.config.app_id = 'jwt_test'
308+
IntercomRails.config.api_secret = 'super-secret'
309+
end
310+
311+
it 'does not include JWT when jwt_enabled is false' do
312+
script_tag = ScriptTag.new(
313+
user_details: { user_id: '1234' },
314+
jwt_enabled: false
315+
)
316+
expect(script_tag.intercom_settings[:intercom_user_jwt]).to be_nil
317+
end
318+
319+
it 'includes JWT when jwt_enabled is true' do
320+
script_tag = ScriptTag.new(
321+
user_details: { user_id: '1234' },
322+
jwt_enabled: true
323+
)
324+
expect(script_tag.intercom_settings[:intercom_user_jwt]).to be_present
325+
end
326+
327+
it 'does not include user_hash when JWT is enabled' do
328+
script_tag = ScriptTag.new(
329+
user_details: { user_id: '1234' },
330+
jwt_enabled: true
331+
)
332+
expect(script_tag.intercom_settings[:user_hash]).to be_nil
333+
end
334+
335+
it 'generates a valid JWT with correct payload' do
336+
user_id = '1234'
337+
script_tag = ScriptTag.new(
338+
user_details: { user_id: user_id },
339+
jwt_enabled: true
340+
)
341+
342+
jwt = script_tag.intercom_settings[:intercom_user_jwt]
343+
decoded_payload = JWT.decode(jwt, 'super-secret', true, { algorithm: 'HS256' })[0]
344+
345+
expect(decoded_payload['user_id']).to eq(user_id)
346+
expect(decoded_payload['exp']).to be_within(5).of(24.hours.from_now.to_i)
347+
end
348+
349+
it 'does not generate JWT when user_id is missing' do
350+
script_tag = ScriptTag.new(
351+
user_details: { email: '[email protected]' },
352+
jwt_enabled: true
353+
)
354+
expect(script_tag.intercom_settings[:intercom_user_jwt]).to be_nil
355+
end
356+
357+
it 'does not generate JWT when api_secret is missing' do
358+
IntercomRails.config.api_secret = nil
359+
script_tag = ScriptTag.new(
360+
user_details: { user_id: '1234' },
361+
jwt_enabled: true
362+
)
363+
expect(script_tag.intercom_settings[:intercom_user_jwt]).to be_nil
364+
end
365+
366+
it 'removes user_id from payload when using JWT' do
367+
script_tag = ScriptTag.new(
368+
user_details: {
369+
user_id: '1234',
370+
371+
name: 'Test User'
372+
},
373+
jwt_enabled: true
374+
)
375+
376+
expect(script_tag.intercom_settings[:intercom_user_jwt]).to be_present
377+
expect(script_tag.intercom_settings[:user_id]).to be_nil
378+
expect(script_tag.intercom_settings[:email]).to eq('[email protected]')
379+
expect(script_tag.intercom_settings[:name]).to eq('Test User')
380+
end
381+
382+
it 'keeps user_id in payload when not using JWT' do
383+
script_tag = ScriptTag.new(
384+
user_details: {
385+
user_id: '1234',
386+
387+
name: 'Test User'
388+
},
389+
jwt_enabled: false
390+
)
391+
392+
expect(script_tag.intercom_settings[:user_id]).to eq('1234')
393+
expect(script_tag.intercom_settings[:email]).to eq('[email protected]')
394+
expect(script_tag.intercom_settings[:name]).to eq('Test User')
395+
end
396+
end
397+
304398
end

spec/spec_helper.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
require 'intercom-rails'
22
require 'rspec'
33
require 'active_support/core_ext/string/output_safety'
4+
require 'pry'
45

56
def dummy_user(options = {})
67
user = Struct.new(:email, :name).new

0 commit comments

Comments
 (0)