diff --git a/intercom-rails.gemspec b/intercom-rails.gemspec index 799c56c..2a78bc8 100644 --- a/intercom-rails.gemspec +++ b/intercom-rails.gemspec @@ -19,6 +19,7 @@ Gem::Specification.new do |s| s.test_files = Dir["test/**/*"] s.add_dependency 'activesupport', '>4.0' + s.add_dependency 'jwt', '~> 2.0' s.add_development_dependency 'rake' s.add_development_dependency 'actionpack', '>5.0' diff --git a/lib/intercom-rails/config.rb b/lib/intercom-rails/config.rb index 1093b2d..78e761f 100644 --- a/lib/intercom-rails/config.rb +++ b/lib/intercom-rails/config.rb @@ -110,6 +110,7 @@ def self.reset! config_accessor :hide_default_launcher config_accessor :api_base config_accessor :encrypted_mode + config_accessor :jwt_enabled def self.api_key=(*) warn "Setting an Intercom API key is no longer supported; remove the `config.api_key = ...` line from config/initializers/intercom.rb" diff --git a/lib/intercom-rails/script_tag.rb b/lib/intercom-rails/script_tag.rb index 47b7fc3..cc1eea6 100644 --- a/lib/intercom-rails/script_tag.rb +++ b/lib/intercom-rails/script_tag.rb @@ -2,6 +2,7 @@ require 'active_support/all' require 'action_view' +require 'jwt' module IntercomRails @@ -17,7 +18,7 @@ class ScriptTag include ::ActionView::Helpers::TagHelper attr_reader :user_details, :company_details, :show_everywhere, :session_duration - attr_accessor :secret, :widget_options, :controller, :nonce, :encrypted_mode_enabled, :encrypted_mode + attr_accessor :secret, :widget_options, :controller, :nonce, :encrypted_mode_enabled, :encrypted_mode, :jwt_enabled def initialize(options = {}) self.secret = options[:secret] || Config.api_secret @@ -25,7 +26,8 @@ def initialize(options = {}) self.controller = options[:controller] @show_everywhere = options[:show_everywhere] @session_duration = session_duration_from_config - + self.jwt_enabled = options[:jwt_enabled] || Config.jwt_enabled + initial_user_details = if options[:find_current_user_details] find_current_user_details else @@ -119,12 +121,30 @@ def intercom_javascript "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);}};})()" end + def generate_jwt + return nil unless user_details[:user_id].present? + + payload = { + user_id: user_details[:user_id].to_s, + exp: 24.hours.from_now.to_i + } + JWT.encode(payload, secret, 'HS256') + end + def user_details=(user_details) @user_details = DateHelper.convert_dates_to_unix_timestamps(user_details || {}) @user_details = @user_details.with_indifferent_access.tap do |u| [:email, :name, :user_id].each { |k| u.delete(k) if u[k].nil? } - u[:user_hash] ||= user_hash if secret.present? && (u[:user_id] || u[:email]).present? + if secret.present? + if jwt_enabled && u[:user_id].present? + u[:intercom_user_jwt] ||= generate_jwt + u.delete(:user_id) # No need to send plaintext user_id when using JWT + elsif (u[:user_id] || u[:email]).present? + u[:user_hash] ||= user_hash + end + end + u[:app_id] ||= app_id end end diff --git a/lib/intercom-rails/version.rb b/lib/intercom-rails/version.rb index e5d3935..b689ae9 100644 --- a/lib/intercom-rails/version.rb +++ b/lib/intercom-rails/version.rb @@ -1,3 +1,3 @@ module IntercomRails - VERSION = "1.0.2" + VERSION = "1.0.3" end diff --git a/spec/config_spec.rb b/spec/config_spec.rb index c84fd26..359e541 100644 --- a/spec/config_spec.rb +++ b/spec/config_spec.rb @@ -114,4 +114,21 @@ IntercomRails.config.user.company_association = Proc.new { [] } end.to output(/no longer supported/).to_stderr end + + it 'gets/sets jwt_enabled' do + IntercomRails.config.jwt_enabled = true + expect(IntercomRails.config.jwt_enabled).to eq(true) + end + + it 'defaults jwt_enabled to nil' do + IntercomRails.config.reset! + expect(IntercomRails.config.jwt_enabled).to eq(nil) + end + + it 'allows jwt_enabled in block form' do + IntercomRails.config do |config| + config.jwt_enabled = true + end + expect(IntercomRails.config.jwt_enabled).to eq(true) + end end diff --git a/spec/script_tag_helper_spec.rb b/spec/script_tag_helper_spec.rb index d6667e4..04134b2 100644 --- a/spec/script_tag_helper_spec.rb +++ b/spec/script_tag_helper_spec.rb @@ -49,4 +49,35 @@ expect(script_tag.to_s).to include('nonce="pJwtLVnwiMaPCxpb41KZguOcC5mGUYD+8RNGcJSlR94="') end end + + context 'JWT authentication' do + before(:each) do + allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new("test")) + end + before(:each) do + IntercomRails.config.api_secret = 'super-secret' + end + + it 'enables JWT when configured' do + IntercomRails.config.jwt_enabled = true + output = intercom_script_tag({ + user_id: '1234', + email: 'test@example.com' + }).to_s + + expect(output).to include('intercom_user_jwt') + expect(output).not_to include('user_hash') + end + + it 'falls back to user_hash when JWT is disabled' do + IntercomRails.config.jwt_enabled = false + output = intercom_script_tag({ + user_id: '1234', + email: 'test@example.com' + }).to_s + + expect(output).not_to include('intercom_user_jwt') + expect(output).to include('user_hash') + end + end end diff --git a/spec/script_tag_spec.rb b/spec/script_tag_spec.rb index 32b6dfe..0df492d 100644 --- a/spec/script_tag_spec.rb +++ b/spec/script_tag_spec.rb @@ -1,5 +1,6 @@ require 'active_support/time' require 'spec_helper' +require 'jwt' describe IntercomRails::ScriptTag do ScriptTag = IntercomRails::ScriptTag @@ -301,4 +302,97 @@ def user end end + context 'JWT authentication' do + before(:each) do + IntercomRails.config.app_id = 'jwt_test' + IntercomRails.config.api_secret = 'super-secret' + end + + it 'does not include JWT when jwt_enabled is false' do + script_tag = ScriptTag.new( + user_details: { user_id: '1234' }, + jwt_enabled: false + ) + expect(script_tag.intercom_settings[:intercom_user_jwt]).to be_nil + end + + it 'includes JWT when jwt_enabled is true' do + script_tag = ScriptTag.new( + user_details: { user_id: '1234' }, + jwt_enabled: true + ) + expect(script_tag.intercom_settings[:intercom_user_jwt]).to be_present + end + + it 'does not include user_hash when JWT is enabled' do + script_tag = ScriptTag.new( + user_details: { user_id: '1234' }, + jwt_enabled: true + ) + expect(script_tag.intercom_settings[:user_hash]).to be_nil + end + + it 'generates a valid JWT with correct payload' do + user_id = '1234' + script_tag = ScriptTag.new( + user_details: { user_id: user_id }, + jwt_enabled: true + ) + + jwt = script_tag.intercom_settings[:intercom_user_jwt] + decoded_payload = JWT.decode(jwt, 'super-secret', true, { algorithm: 'HS256' })[0] + + expect(decoded_payload['user_id']).to eq(user_id) + expect(decoded_payload['exp']).to be_within(5).of(24.hours.from_now.to_i) + end + + it 'does not generate JWT when user_id is missing' do + script_tag = ScriptTag.new( + user_details: { email: 'test@example.com' }, + jwt_enabled: true + ) + expect(script_tag.intercom_settings[:intercom_user_jwt]).to be_nil + end + + it 'does not generate JWT when api_secret is missing' do + IntercomRails.config.api_secret = nil + script_tag = ScriptTag.new( + user_details: { user_id: '1234' }, + jwt_enabled: true + ) + expect(script_tag.intercom_settings[:intercom_user_jwt]).to be_nil + end + + it 'removes user_id from payload when using JWT' do + script_tag = ScriptTag.new( + user_details: { + user_id: '1234', + email: 'test@example.com', + name: 'Test User' + }, + jwt_enabled: true + ) + + expect(script_tag.intercom_settings[:intercom_user_jwt]).to be_present + expect(script_tag.intercom_settings[:user_id]).to be_nil + expect(script_tag.intercom_settings[:email]).to eq('test@example.com') + expect(script_tag.intercom_settings[:name]).to eq('Test User') + end + + it 'keeps user_id in payload when not using JWT' do + script_tag = ScriptTag.new( + user_details: { + user_id: '1234', + email: 'test@example.com', + name: 'Test User' + }, + jwt_enabled: false + ) + + expect(script_tag.intercom_settings[:user_id]).to eq('1234') + expect(script_tag.intercom_settings[:email]).to eq('test@example.com') + expect(script_tag.intercom_settings[:name]).to eq('Test User') + end + end + end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 0fc4351..151704b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,6 +1,7 @@ require 'intercom-rails' require 'rspec' require 'active_support/core_ext/string/output_safety' +require 'pry' def dummy_user(options = {}) user = Struct.new(:email, :name).new