diff --git a/app/controllers/concerns/hmac_concern.rb b/app/controllers/concerns/hmac_concern.rb index abc55a394..25a0145c6 100644 --- a/app/controllers/concerns/hmac_concern.rb +++ b/app/controllers/concerns/hmac_concern.rb @@ -1,5 +1,30 @@ module HmacConcern def hmac_verified? - ActiveModel::Type::Boolean.new.cast(params[:hmac_verified]).present? + return false unless hmac_verification_params_valid? + + expected_hash = OpenSSL::HMAC.hexdigest( + 'sha256', + hmac_channel.hmac_token, + @contact.identifier + ) + identifier_hash = params[:identifier_hash].to_s + return false unless identifier_hash.bytesize == expected_hash.bytesize + + ActiveSupport::SecurityUtils.secure_compare(identifier_hash, expected_hash) + end + + private + + def hmac_verification_params_valid? + params[:identifier_hash].present? && + @contact&.identifier.present? && + params[:identifier].to_s == @contact.identifier && + hmac_channel.present? + end + + def hmac_channel + return if @inbox.blank? + + @hmac_channel ||= @inbox.channel if @inbox.channel.respond_to?(:hmac_token) end end diff --git a/spec/controllers/api/v1/accounts/contacts/contact_inboxes_controller_spec.rb b/spec/controllers/api/v1/accounts/contacts/contact_inboxes_controller_spec.rb index 659d41f4a..1d40c98cb 100644 --- a/spec/controllers/api/v1/accounts/contacts/contact_inboxes_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/contacts/contact_inboxes_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' RSpec.describe '/api/v1/accounts/{account.id}/contacts/:id/contact_inboxes', type: :request do let(:account) { create(:account) } - let(:contact) { create(:contact, account: account, email: 'f.o.o.b.a.r@gmail.com') } + let(:contact) { create(:contact, account: account, email: 'f.o.o.b.a.r@gmail.com', identifier: 'contact-identifier') } let(:channel_twilio_sms) { create(:channel_twilio_sms, account: account) } let(:channel_email) { create(:channel_email, account: account) } let(:channel_api) { create(:channel_api, account: account) } @@ -45,7 +45,7 @@ RSpec.describe '/api/v1/accounts/{account.id}/contacts/:id/contact_inboxes', typ expect(contact.reload.contact_inboxes.map(&:inbox_id)).to include(channel_email.inbox.id) end - it 'creates an hmac verified contact inbox' do + it 'does not trust client supplied hmac_verified param' do create(:inbox_member, inbox: channel_api.inbox, user: agent) expect do post "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/contact_inboxes", @@ -54,6 +54,23 @@ RSpec.describe '/api/v1/accounts/{account.id}/contacts/:id/contact_inboxes', typ as: :json end.to change(ContactInbox, :count).by(1) + expect(response).to have_http_status(:success) + contact_inbox = contact.reload.contact_inboxes.find_by(inbox_id: channel_api.inbox.id) + expect(contact_inbox).to be_present + expect(contact_inbox.hmac_verified).to be(false) + end + + it 'creates an hmac verified contact inbox with a valid identifier hash' do + create(:inbox_member, inbox: channel_api.inbox, user: agent) + identifier_hash = OpenSSL::HMAC.hexdigest('sha256', channel_api.hmac_token, contact.identifier) + + expect do + post "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/contact_inboxes", + params: { inbox_id: channel_api.inbox.id, identifier: contact.identifier, identifier_hash: identifier_hash }, + headers: agent.create_new_auth_token, + as: :json + end.to change(ContactInbox, :count).by(1) + expect(response).to have_http_status(:success) contact_inbox = contact.reload.contact_inboxes.find_by(inbox_id: channel_api.inbox.id) expect(contact_inbox).to be_present