Skip to content

Commit

Permalink
Support experimental JWT feature (not available yet) (#356)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
DamonFstr authored Dec 19, 2024
1 parent 3296abf commit 0404f08
Show file tree
Hide file tree
Showing 8 changed files with 169 additions and 4 deletions.
1 change: 1 addition & 0 deletions intercom-rails.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
1 change: 1 addition & 0 deletions lib/intercom-rails/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
26 changes: 23 additions & 3 deletions lib/intercom-rails/script_tag.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require 'active_support/all'
require 'action_view'
require 'jwt'

module IntercomRails

Expand All @@ -17,15 +18,16 @@ 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
self.widget_options = widget_options_from_config.merge(options[:widget] || {})
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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/intercom-rails/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module IntercomRails
VERSION = "1.0.2"
VERSION = "1.0.3"
end
17 changes: 17 additions & 0 deletions spec/config_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
31 changes: 31 additions & 0 deletions spec/script_tag_helper_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
94 changes: 94 additions & 0 deletions spec/script_tag_spec.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require 'active_support/time'
require 'spec_helper'
require 'jwt'

describe IntercomRails::ScriptTag do
ScriptTag = IntercomRails::ScriptTag
Expand Down Expand Up @@ -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
1 change: 1 addition & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
@@ -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
Expand Down

0 comments on commit 0404f08

Please sign in to comment.