Skip to content

Support signing data attributes with JWT #358

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 2 commits into from
Dec 20, 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
10 changes: 9 additions & 1 deletion lib/intercom-rails/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,6 @@ 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 Expand Up @@ -144,6 +143,15 @@ def self.company_association=(*)
end
end

config_group :jwt do
config_accessor :enabled
config_accessor :signed_user_fields do |value|
unless value.nil? || (value.kind_of?(Array) && value.all? { |v| v.kind_of?(Symbol) || v.kind_of?(String) })
raise ArgumentError, "jwt.signed_user_fields must be an array of symbols or strings"
end
end
end

end

end
16 changes: 14 additions & 2 deletions lib/intercom-rails/script_tag.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ 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
self.jwt_enabled = options[:jwt_enabled] || Config.jwt.enabled

initial_user_details = if options[:find_current_user_details]
find_current_user_details
Expand Down Expand Up @@ -128,6 +128,14 @@ def generate_jwt
user_id: user_details[:user_id].to_s,
exp: 24.hours.from_now.to_i
}

if Config.jwt.signed_user_fields.present?
Config.jwt.signed_user_fields.each do |field|
field = field.to_sym
payload[field] = user_details[field].to_s if user_details[field].present?
end
end

JWT.encode(payload, secret, 'HS256')
end

Expand All @@ -139,7 +147,11 @@ def user_details=(user_details)
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

u.delete(:user_id)
Config.jwt.signed_user_fields&.each do |field|
u.delete(field.to_sym)
end
elsif (u[:user_id] || u[:email]).present?
u[:user_hash] ||= user_hash
end
Expand Down
52 changes: 40 additions & 12 deletions spec/config_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -115,20 +115,48 @@
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
context 'jwt configuration' do

it 'defaults jwt_enabled to nil' do
IntercomRails.config.reset!
expect(IntercomRails.config.jwt_enabled).to eq(nil)
end
it 'gets/sets jwt_enabled' do
IntercomRails.config.jwt.enabled = true
expect(IntercomRails.config.jwt.enabled).to eq(true)
end

it 'allows jwt_enabled in block form' do
IntercomRails.config do |config|
config.jwt_enabled = true
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\

it 'gets/sets signed_user_fields' do
IntercomRails.config.jwt.signed_user_fields = [:email, :name]
expect(IntercomRails.config.jwt.signed_user_fields).to eq([:email, :name])
end

it 'validates signed_user_fields is an array of symbols or strings' do
expect {
IntercomRails.config.jwt.signed_user_fields = "not_an_array"
}.to raise_error(ArgumentError)

expect {
IntercomRails.config.jwt.signed_user_fields = [1, 2, 3]
}.to raise_error(ArgumentError)

expect {
IntercomRails.config.jwt.signed_user_fields = [:email, "name", :custom_field]
}.not_to raise_error
end

it 'allows nil signed_user_fields' do
expect {
IntercomRails.config.jwt.signed_user_fields = nil
}.not_to raise_error
end
expect(IntercomRails.config.jwt_enabled).to eq(true)
end
end
4 changes: 2 additions & 2 deletions spec/script_tag_helper_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
end

it 'enables JWT when configured' do
IntercomRails.config.jwt_enabled = true
IntercomRails.config.jwt.enabled = true
output = intercom_script_tag({
user_id: '1234',
email: 'test@example.com'
Expand All @@ -70,7 +70,7 @@
end

it 'falls back to user_hash when JWT is disabled' do
IntercomRails.config.jwt_enabled = false
IntercomRails.config.jwt.enabled = false
output = intercom_script_tag({
user_id: '1234',
email: 'test@example.com'
Expand Down
74 changes: 74 additions & 0 deletions spec/script_tag_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,80 @@ def user
expect(script_tag.intercom_settings[:email]).to eq('test@example.com')
expect(script_tag.intercom_settings[:name]).to eq('Test User')
end

context 'with signed_user_fields' do
before do
IntercomRails.config.jwt.signed_user_fields = [:email, :name, :plan, :team_id]
end

it 'includes configured fields in JWT when present' do
script_tag = ScriptTag.new(
user_details: {
user_id: '1234',
email: 'test@example.com',
plan: 'pro',
team_id: 'team_123',
company_size: 100
},
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('1234')
expect(decoded_payload['email']).to eq('test@example.com')
expect(decoded_payload['plan']).to eq('pro')
expect(decoded_payload['team_id']).to eq('team_123')
expect(decoded_payload['company_size']).to be_nil

expect(script_tag.intercom_settings[:user_id]).to be_nil
expect(script_tag.intercom_settings[:email]).to be_nil
expect(script_tag.intercom_settings[:plan]).to be_nil
expect(script_tag.intercom_settings[:team_id]).to be_nil
expect(script_tag.intercom_settings[:company_size]).to eq(100)
end

it 'handles missing configured fields gracefully' do
script_tag = ScriptTag.new(
user_details: {
user_id: '1234',
email: 'test@example.com'
},
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('1234')
expect(decoded_payload['email']).to eq('test@example.com')
expect(decoded_payload['name']).to be_nil
end

it 'respects empty signed_user_fields configuration' do
IntercomRails.config.jwt.signed_user_fields = []
script_tag = ScriptTag.new(
user_details: {
user_id: '1234',
email: 'test@example.com',
name: 'Test User'
},
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('1234')
expect(decoded_payload['email']).to be_nil
expect(decoded_payload['name']).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
end
end

end