From d98ffb6dd055239d2b30a56a48e13513019898c4 Mon Sep 17 00:00:00 2001 From: Jay Wolff Date: Thu, 2 Jan 2025 13:07:30 -0500 Subject: [PATCH 1/4] bugfix, raise Missing Twilio credentials if Twilio credentials are blank --- app/services/twilio_verify_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/twilio_verify_service.rb b/app/services/twilio_verify_service.rb index 8c03a69..f10acce 100644 --- a/app/services/twilio_verify_service.rb +++ b/app/services/twilio_verify_service.rb @@ -47,7 +47,7 @@ def initialize @twilio_auth_token = Rails.application.credentials.twilio_auth_token || ENV['TWILIO_AUTH_TOKEN'] @twilio_verify_service_sid = Rails.application.credentials.twilio_verify_service_sid || ENV['TWILIO_VERIFY_SERVICE_SID'] - raise 'Missing Twilio credentials' unless @twilio_account_sid && @twilio_auth_token && @twilio_verify_service_sid + raise 'Missing Twilio credentials' if @twilio_account_sid.blank? || @twilio_auth_token.blank? || @twilio_verify_service_sid.blank? @twilio_client = Twilio::REST::Client.new(@twilio_account_sid, @twilio_auth_token) end From d13d617c5961b86dd8b009d053a919d5b671fdef Mon Sep 17 00:00:00 2001 From: Jay Wolff Date: Thu, 2 Jan 2025 13:07:41 -0500 Subject: [PATCH 2/4] Add unit tests for TwilioVerifyService --- spec/services/twilio_verify_service_spec.rb | 125 ++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 spec/services/twilio_verify_service_spec.rb diff --git a/spec/services/twilio_verify_service_spec.rb b/spec/services/twilio_verify_service_spec.rb new file mode 100644 index 0000000..c89196f --- /dev/null +++ b/spec/services/twilio_verify_service_spec.rb @@ -0,0 +1,125 @@ +# frozen_string_literal: true + +RSpec.describe TwilioVerifyService, type: :service do + let(:user) { create :twilio_verify_user } + let(:phone_number) { user.mobile_phone } + let(:formatted_phone_number) { "+1#{phone_number}" } + let(:twilio_account_sid) { '123456789' } + let(:twilio_auth_token) { '123456789' } + let(:twilio_verify_service_sid) { '123456789' } + + before do + allow(Rails.application.credentials).to receive(:twilio_account_sid).and_return twilio_account_sid + allow(Rails.application.credentials).to receive(:twilio_auth_token).and_return twilio_auth_token + allow(Rails.application.credentials).to receive(:twilio_verify_service_sid).and_return twilio_verify_service_sid + end + + describe 'Missing Twilio credentials' do + let(:twilio_auth_token) { '' } + + it "raises 'Missing Twilio credentials' exception if any credentials are missing" do + expect { described_class.new }.to raise_error 'Missing Twilio credentials' + end + end + + # https://www.twilio.com/docs/verify/sms + describe 'SMS 2FA' do + let(:twilio_client) { Twilio::REST::Client.new(twilio_account_sid, twilio_auth_token) } + let(:twilio_client_verify_service) { double('Twilio::REST::Verify::V2::ServiceContext') } + + before do + allow(Twilio::REST::Client).to receive(:new).with(twilio_account_sid, twilio_auth_token).and_return twilio_client + allow(twilio_client).to receive_message_chain(:verify, :services).with(twilio_verify_service_sid).and_return twilio_client_verify_service + end + + describe '.send_sms_token' do + it 'calls on the Twilio Verify API to send a one-time code to the given phone number via SMS' do + expect(twilio_client_verify_service).to receive_message_chain(:verifications, :create).with(to: formatted_phone_number, channel: 'sms') + described_class.send_sms_token(phone_number) + end + end + + describe '.verify_sms_token' do + let(:token) { '123456' } + + it 'calls on the Twilio Verify API to verify a one-time code that was previously sent to the given phone number via SMS' do + expect(twilio_client_verify_service).to receive_message_chain(:verification_checks, :create).with(to: formatted_phone_number, code: token) + described_class.verify_sms_token(phone_number, token) + end + end + end + + # https://www.twilio.com/docs/verify/quickstarts/totp + describe 'TOTP 2FA' do + let(:twilio_client) { Twilio::REST::Client.new(twilio_account_sid, twilio_auth_token) } + let(:twilio_client_verify_service) { double('Twilio::REST::Verify::V2::ServiceContext') } + let(:twilio_client_entity) { double('Twilio::REST::Verify::V2::ServiceContext::EntityContext') } + + before do + allow(Twilio::REST::Client).to receive(:new).with(twilio_account_sid, twilio_auth_token).and_return twilio_client + allow(twilio_client).to receive_message_chain(:verify, :v2, :services).with(twilio_verify_service_sid).and_return twilio_client_verify_service + allow(twilio_client_verify_service).to receive(:entities).with("test-#{user.id}").and_return twilio_client_entity + end + + describe '.setup_totp_service' do + it 'calls on the Twilio Verify API to setup TOTP for a given user' do + new_factor = double(:twilio_verify_totp_new_factor, sid: '123ABC') + expect(twilio_client_entity).to receive_message_chain(:new_factors, :create).with(friendly_name: user.to_s, factor_type: 'totp').and_return new_factor + + expect(user.twilio_totp_factor_sid).to be_nil + result = described_class.setup_totp_service(user) + + expect(user.reload.twilio_totp_factor_sid).to eq result.sid + end + end + + describe '.register_totp_service' do + let(:token) { '123456' } + let(:new_factor) { double(:twilio_verify_totp_new_factor, status: 'verified') } + + before do + user.update!(twilio_totp_factor_sid: '123ABC') + allow(twilio_client_entity).to receive(:factors).with(user.twilio_totp_factor_sid).and_return new_factor + end + + it 'calls on the Twilio Verify API to register TOTP for a given user' do + expect(new_factor).to receive(:update).with(auth_payload: token).and_return new_factor + + result = described_class.register_totp_service(user, token) + expect(result.status).to eq 'verified' + end + + context 'when an invalid token is provided' do + let(:new_factor) { double(:twilio_verify_totp_new_factor, status: 'unverified') } + + it 'returns an unverified status' do + expect(new_factor).to receive(:update).with(auth_payload: token).and_return new_factor + + result = described_class.register_totp_service(user, token) + expect(result.status).to eq 'unverified' + end + end + end + + describe '.verify_totp_token' do + let(:token) { '123456' } + + before { user.update!(twilio_totp_factor_sid: '123ABC') } + + it 'calls on the Twilio Verify API to verify a TOTP based one time code for a given user' do + expect(twilio_client_entity).to receive_message_chain(:challenges, :create).with(auth_payload: token, factor_sid: user.twilio_totp_factor_sid) + described_class.verify_totp_token(user, token) + end + end + end + + describe '.e164_format' do + # https://en.wikipedia.org/wiki/E.164 + it 'formats supplied phone number to the e164 format' do + expect(described_class.e164_format(phone_number)).to eq formatted_phone_number + expect(described_class.e164_format('(123) 456-7890')).to eq '+11234567890' + expect(described_class.e164_format('123-456-7890')).to eq '+11234567890' + expect(described_class.e164_format('1234567890')).to eq '+11234567890' + end + end +end From 6ac15135bdd57bc95c892237fee02c3d7e091ec9 Mon Sep 17 00:00:00 2001 From: Jay Wolff Date: Thu, 2 Jan 2025 13:38:59 -0500 Subject: [PATCH 3/4] note required_ruby_version is at least 2.5 and update gem summary / description --- devise-twilio-verify.gemspec | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/devise-twilio-verify.gemspec b/devise-twilio-verify.gemspec index 444351d..27bf9de 100644 --- a/devise-twilio-verify.gemspec +++ b/devise-twilio-verify.gemspec @@ -8,11 +8,11 @@ Gem::Specification.new do |spec| spec.name = "devise-twilio-verify" spec.version = DeviseTwilioVerify::VERSION spec.authors = ["Jay Wolff"] - - spec.summary = %q{Devise-Twilio-Verify is a Devise extension that adds two-factor authentication (2FA) support using SMS and/or TOTP via the Twilio Verify API} - spec.description = %q{The devise-twilio-verify gem is an extension for the Devise authentication system that adds extra security with two-factor authentication (2FA). It leverages the Twilio Verify API to send verification codes to users via SMS or TOTP (time-based codes). This gem makes it easy to set up 2FA in your Devise-powered Rails app, helping to keep user accounts secure. This gem is meant to make migrating from authy to twilio verify as simple as possible, please see the README for details.} + spec.summary = %q{Devise plugin for two-factor authentication (2FA) using SMS/TOTP with Twilio Verify API} + spec.description = %q{The devise-twilio-verify gem extends the Devise authentication system to provide enhanced security through two-factor authentication (2FA). It integrates with the Twilio Verify API to send verification codes via SMS or TOTP (time-based one-time passwords). This gem simplifies adding 2FA to Devise-powered Rails applications, ensuring better protection for user accounts. For instructions on migrating from the legacy Authy API (devise-authy) to Twilio Verify, please refer to the README.} spec.homepage = "https://github.com/jayywolff/twilio-verify-devise" spec.license = "MIT" + spec.required_ruby_version = '>= 2.5.0' spec.metadata = { "bug_tracker_uri" => "https://github.com/jayywolff/twilio-verify-devise/issues", From 4cf12686b789a03626251291a54c53ee46e8bca2 Mon Sep 17 00:00:00 2001 From: Jay Wolff Date: Thu, 2 Jan 2025 13:39:28 -0500 Subject: [PATCH 4/4] bump version 0.2.5 --- CHANGELOG.md | 8 ++++++++ lib/devise-twilio-verify/version.rb | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b698c9..e682a0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [0.2.5] - 2025-01-02 + +### Changed + +- Raise "Missing Twilio Credentials" when Twilio credentials are nil or empty strings. +- Added test coverage for TwilioVerifyService, (the abstraction layer for Twilio Verify API calls) +- Updated gemspec summary/description again + ## [0.2.4] - 2024-12-29 ### Changed diff --git a/lib/devise-twilio-verify/version.rb b/lib/devise-twilio-verify/version.rb index 1e68a54..c243e20 100644 --- a/lib/devise-twilio-verify/version.rb +++ b/lib/devise-twilio-verify/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module DeviseTwilioVerify - VERSION = '0.2.4' + VERSION = '0.2.5' end