Skip to content

Support experimental JWT feature (not available yet) #356

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Contributor Author

@DamonFstr DamonFstr Dec 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll likely tweak this down, placeholder for now really. More than likely make it configurable too.

}
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