From c820dd01d9f5952a1b01c243da61ea4b7df1f3f9 Mon Sep 17 00:00:00 2001
From: Joao Santos <jpsantos@gmail.com>
Date: Tue, 24 Oct 2023 15:34:11 -0300
Subject: [PATCH 1/7] add SwWebhookAuthentication middleware

---
 lib/rack/signalwire_webhook_authentication.rb |  53 +++++
 signalwire.gemspec                            |   1 +
 .../signalwire_webhook_authentication_spec.rb | 197 ++++++++++++++++++
 spec/spec_helper.rb                           |   1 +
 4 files changed, 252 insertions(+)
 create mode 100644 lib/rack/signalwire_webhook_authentication.rb
 create mode 100644 spec/rack/signalwire_webhook_authentication_spec.rb

diff --git a/lib/rack/signalwire_webhook_authentication.rb b/lib/rack/signalwire_webhook_authentication.rb
new file mode 100644
index 0000000..e594447
--- /dev/null
+++ b/lib/rack/signalwire_webhook_authentication.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'rack/media_type'
+
+module Rack
+
+  class SwWebhookAuthentication
+
+    FORM_URLENCODED_MEDIA_TYPE = Rack::MediaType.type('application/x-www-form-urlencoded')
+
+    def initialize(app, private_key, *paths, &private_key_lookup)
+      @app = app
+      @private_key = private_key
+      define_singleton_method(:get_private_key, private_key_lookup) if block_given?
+      @path_regex = Regexp.union(paths)
+    end
+
+    def call(env)
+      return @app.call(env) unless env['PATH_INFO'].match(@path_regex)
+      request = Rack::Request.new(env)
+      original_url = request.url
+      params = extract_params!(request)
+      private_key = @private_key || get_private_key(params['AccountSid'])
+      validator = Signalwire::Webhook::ValidateRequest.new(private_key)
+      signature = env['HTTP_X_SIGNALWIRE_SIGNATURE'] || env['HTTP_X_TWILIO_SIGNATURE'] || ''
+      if validator.validate(original_url, params, signature)
+        @app.call(env)
+      else
+        [
+          403,
+          { 'Content-Type' => 'text/plain' },
+          ['Signalwire Request Validation Failed.']
+        ]
+      end
+    end
+
+    def extract_params!(request)
+      return {} unless request.post?
+
+      if request.media_type == FORM_URLENCODED_MEDIA_TYPE
+        request.POST
+      else
+        request.body.rewind
+        body = request.body.read
+        request.body.rewind
+        body
+      end
+    end
+
+    private :extract_params!
+
+  end
+end
diff --git a/signalwire.gemspec b/signalwire.gemspec
index 5113569..30bedd1 100644
--- a/signalwire.gemspec
+++ b/signalwire.gemspec
@@ -27,6 +27,7 @@ Gem::Specification.new do |spec|
   spec.add_development_dependency 'bundler', '~> 2.1'
   spec.add_development_dependency 'bundler-audit', '~> 0.6'
   spec.add_development_dependency 'guard-rspec', '~> 4.7'
+  spec.add_development_dependency 'rack', '~> 2.0'
   spec.add_development_dependency 'rake', '~> 13.0'
   spec.add_development_dependency 'rdoc', '~> 6.1'
   spec.add_development_dependency 'rspec', '~> 3.0'
diff --git a/spec/rack/signalwire_webhook_authentication_spec.rb b/spec/rack/signalwire_webhook_authentication_spec.rb
new file mode 100644
index 0000000..b90d88b
--- /dev/null
+++ b/spec/rack/signalwire_webhook_authentication_spec.rb
@@ -0,0 +1,197 @@
+require 'spec_helper'
+require 'rack/mock'
+require 'rack/signalwire_webhook_authentication'
+
+describe Rack::SwWebhookAuthentication do
+  before do
+    @app = ->(_env) { [200, { 'Content-Type' => 'text/plain' }, ['Hello']] }
+  end
+
+  describe 'new' do
+    it 'should initialize with an app, auth token and a path' do
+      expect do
+        Rack::SwWebhookAuthentication.new(@app, 'ABC', /\/voice/)
+      end.not_to raise_error
+    end
+
+    it 'should initialize with an app, auth token and paths' do
+      expect do
+        Rack::SwWebhookAuthentication.new(@app, 'ABC', /\/voice/, /\/sms/)
+      end.not_to raise_error
+    end
+
+    it 'should initialize with an app, dynamic token and paths' do
+      expect do
+        Rack::SwWebhookAuthentication.new(@app, nil, /\/voice/, /\/sms/)
+      end.not_to raise_error
+    end
+  end
+
+  describe 'calling against one path with dynamic auth token' do
+    it 'should allow a request through if it validates' do
+      auth_token = 'qwerty'
+      account_sid = 12_345
+      expect_any_instance_of(Rack::Request).to receive(:post?).and_return(true)
+      expect_any_instance_of(Rack::Request).to receive(:media_type).and_return(Rack::MediaType.type('application/x-www-form-urlencoded'))
+      expect_any_instance_of(Rack::Request).to receive(:POST).and_return({ 'AccountSid' => account_sid })
+      @middleware = Rack::SwWebhookAuthentication.new(@app, nil, /\/voice/) { |asid| auth_token }
+      request_validator = double('RequestValidator')
+      expect(Twilio::Security::RequestValidator).to receive(:new).with(auth_token).and_return(request_validator)
+      expect(request_validator).to receive(:validate).and_return(true)
+      request = Rack::MockRequest.env_for('/voice')
+      status, headers, body = @middleware.call(request)
+      expect(status).to be(200)
+    end
+  end
+
+  describe 'calling against one path' do
+    before do
+      @middleware = Rack::SwWebhookAuthentication.new(@app, 'ABC', /\/voice/)
+    end
+
+    it 'should not intercept when the path doesn\'t match' do
+      expect_any_instance_of(Twilio::Security::RequestValidator).to_not receive(:validate)
+      request = Rack::MockRequest.env_for('/sms')
+      status, headers, body = @middleware.call(request)
+      expect(status).to be(200)
+    end
+
+    it 'should allow a request through if it validates' do
+      expect_any_instance_of(Twilio::Security::RequestValidator).to(
+        receive(:validate).and_return(true)
+      )
+      request = Rack::MockRequest.env_for('/voice')
+      status, headers, body = @middleware.call(request)
+      expect(status).to be(200)
+    end
+
+    it 'should short circuit a request to 403 if it does not validate' do
+      expect_any_instance_of(Twilio::Security::RequestValidator).to(
+        receive(:validate).and_return(false)
+      )
+      request = Rack::MockRequest.env_for('/voice')
+      status, headers, body = @middleware.call(request)
+      expect(status).to be(403)
+    end
+  end
+
+  describe 'calling against many paths' do
+    before do
+      @middleware = Rack::SwWebhookAuthentication.new(@app, 'ABC', /\/voice/, /\/sms/)
+    end
+
+    it 'should not intercept when the path doesn\'t match' do
+      expect_any_instance_of(Twilio::Security::RequestValidator).to_not receive(:validate)
+      request = Rack::MockRequest.env_for('icesms')
+      status, headers, body = @middleware.call(request)
+      expect(status).to be(200)
+    end
+
+    it 'shold allow a request through if it validates' do
+      expect_any_instance_of(Twilio::Security::RequestValidator).to(
+        receive(:validate).and_return(true)
+      )
+      request = Rack::MockRequest.env_for('/sms')
+      status, headers, body = @middleware.call(request)
+      expect(status).to be(200)
+    end
+
+    it 'should short circuit a request to 403 if it does not validate' do
+      expect_any_instance_of(Twilio::Security::RequestValidator).to(
+        receive(:validate).and_return(false)
+      )
+      request = Rack::MockRequest.env_for('/sms')
+      status, headers, body = @middleware.call(request)
+      expect(status).to be(403)
+    end
+  end
+
+  describe 'validating non-form-data POST payloads' do
+    it 'should fail if the body does not validate' do
+      middleware = Rack::SwWebhookAuthentication.new(@app, 'qwerty', /\/test/)
+      input = StringIO.new('{"message": "a post body that does not match the bodySHA256"}')
+
+      request = Rack::MockRequest.env_for(
+        'https://example.com/test?bodySHA256=79bfb0acaf0045fd30f13d48d4fe296b393d85a3bfbee881a0172b2bd574b11e',
+        method: 'POST',
+        input: input
+      )
+      request['HTTP_X_TWILIO_SIGNATURE'] = '+LYlbGr/VmN84YPJQCuWs+9UA7E='
+      request['CONTENT_TYPE'] = 'application/json'
+
+      status, headers, body = middleware.call(request)
+
+      expect(status).not_to be(200)
+    end
+
+    it 'should validate if the body signature is correct' do
+      middleware = Rack::SwWebhookAuthentication.new(@app, 'qwerty', /\/test/)
+      input = StringIO.new('{"message": "a post body"}')
+
+      request = Rack::MockRequest.env_for(
+        'https://example.com/test?bodySHA256=8d90d640c6ba47d595ac56203d7f5c6b511be80fdf44a2055acca75a119b9fd2',
+        method: 'POST',
+        input: input
+      )
+      request['HTTP_X_SIGNALWIRE_SIGNATURE'] = 'zR5Oq4f6cijN5oz5bisiVuxYnTU='
+      request['CONTENT_TYPE'] = 'application/json'
+
+      status, headers, body = middleware.call(request)
+
+      expect(status).to be(200)
+    end
+
+    it 'should validate even if a previous middleware read the body first' do
+      middleware = Rack::SwWebhookAuthentication.new(@app, 'qwerty', /\/test/)
+      input = StringIO.new('{"message": "a post body"}')
+
+      request = Rack::MockRequest.env_for(
+        'https://example.com/test?bodySHA256=8d90d640c6ba47d595ac56203d7f5c6b511be80fdf44a2055acca75a119b9fd2',
+        method: 'POST',
+        input: input
+      )
+      request['HTTP_X_SIGNALWIRE_SIGNATURE'] = 'zR5Oq4f6cijN5oz5bisiVuxYnTU='
+      request['CONTENT_TYPE'] = 'application/json'
+      request['rack.input'].read
+
+      status, headers, body = middleware.call(request)
+
+      expect(status).to be(200)
+    end
+  end
+
+  describe 'validating application/x-www-form-urlencoded POST payloads' do
+    it 'should fail if the body does not validate' do
+      middleware = Rack::SwWebhookAuthentication.new(@app, 'qwerty', /\/test/)
+
+      request = Rack::MockRequest.env_for(
+        'https://example.com/test',
+        method: 'POST',
+        params: { 'foo' => 'bar' }
+      )
+      request['HTTP_X_SIGNALWIRE_SIGNATURE'] = 'foobarbaz'
+      expect(request['CONTENT_TYPE']).to eq('application/x-www-form-urlencoded')
+
+      status, headers, body = middleware.call(request)
+
+      expect(status).not_to be(200)
+    end
+
+    it 'should validate if the body signature is correct' do
+      middleware = Rack::SwWebhookAuthentication.new(@app, 'qwerty', /\/test/)
+
+      request = Rack::MockRequest.env_for(
+        'https://example.com/test',
+        method: 'POST',
+        params: { 'foo' => 'bar' }
+      )
+      request['HTTP_X_SIGNALWIRE_SIGNATURE'] = 'TR9Skm9jiF4WVRJznU5glK5I83k='
+      expect(request['CONTENT_TYPE']).to eq('application/x-www-form-urlencoded')
+
+      status, headers, body = middleware.call(request)
+
+      expect(status).to be(200)
+    end
+
+  end
+end
\ No newline at end of file
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 799620d..c7be093 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -5,6 +5,7 @@
 
 %w[
   bundler/setup
+  rack
   signalwire
   webmock/rspec
   vcr

From 9927d0fae70340a5d7c2355b3d6c5c04e1ea5d04 Mon Sep 17 00:00:00 2001
From: Joao Santos <jpsantos@gmail.com>
Date: Tue, 24 Oct 2023 21:48:29 -0300
Subject: [PATCH 2/7] rename filename to match

---
 ...e_webhook_authentication.rb => sw_webhook_authentication.rb} | 0
 ...authentication_spec.rb => sw_webhook_authentication_spec.rb} | 2 +-
 2 files changed, 1 insertion(+), 1 deletion(-)
 rename lib/rack/{signalwire_webhook_authentication.rb => sw_webhook_authentication.rb} (100%)
 rename spec/rack/{signalwire_webhook_authentication_spec.rb => sw_webhook_authentication_spec.rb} (99%)

diff --git a/lib/rack/signalwire_webhook_authentication.rb b/lib/rack/sw_webhook_authentication.rb
similarity index 100%
rename from lib/rack/signalwire_webhook_authentication.rb
rename to lib/rack/sw_webhook_authentication.rb
diff --git a/spec/rack/signalwire_webhook_authentication_spec.rb b/spec/rack/sw_webhook_authentication_spec.rb
similarity index 99%
rename from spec/rack/signalwire_webhook_authentication_spec.rb
rename to spec/rack/sw_webhook_authentication_spec.rb
index b90d88b..2215fbe 100644
--- a/spec/rack/signalwire_webhook_authentication_spec.rb
+++ b/spec/rack/sw_webhook_authentication_spec.rb
@@ -1,6 +1,6 @@
 require 'spec_helper'
 require 'rack/mock'
-require 'rack/signalwire_webhook_authentication'
+require 'rack/sw_webhook_authentication'
 
 describe Rack::SwWebhookAuthentication do
   before do

From f00c9a9d6b868d3e7ec937c663c9ed720a2e6d7c Mon Sep 17 00:00:00 2001
From: Joao Santos <jpsantos@gmail.com>
Date: Wed, 25 Oct 2023 22:03:50 -0300
Subject: [PATCH 3/7] revert to SignalwireWebhookAuthentication

---
 ...b => signalwire_webhook_authentication.rb} |  2 +-
 ...signalwire_webhook_authentication_spec.rb} | 26 +++++++++----------
 2 files changed, 14 insertions(+), 14 deletions(-)
 rename lib/rack/{sw_webhook_authentication.rb => signalwire_webhook_authentication.rb} (97%)
 rename spec/rack/{sw_webhook_authentication_spec.rb => signalwire_webhook_authentication_spec.rb} (85%)

diff --git a/lib/rack/sw_webhook_authentication.rb b/lib/rack/signalwire_webhook_authentication.rb
similarity index 97%
rename from lib/rack/sw_webhook_authentication.rb
rename to lib/rack/signalwire_webhook_authentication.rb
index e594447..1c21263 100644
--- a/lib/rack/sw_webhook_authentication.rb
+++ b/lib/rack/signalwire_webhook_authentication.rb
@@ -4,7 +4,7 @@
 
 module Rack
 
-  class SwWebhookAuthentication
+  class SignalwireWebhookAuthentication
 
     FORM_URLENCODED_MEDIA_TYPE = Rack::MediaType.type('application/x-www-form-urlencoded')
 
diff --git a/spec/rack/sw_webhook_authentication_spec.rb b/spec/rack/signalwire_webhook_authentication_spec.rb
similarity index 85%
rename from spec/rack/sw_webhook_authentication_spec.rb
rename to spec/rack/signalwire_webhook_authentication_spec.rb
index 2215fbe..39cd853 100644
--- a/spec/rack/sw_webhook_authentication_spec.rb
+++ b/spec/rack/signalwire_webhook_authentication_spec.rb
@@ -1,8 +1,8 @@
 require 'spec_helper'
 require 'rack/mock'
-require 'rack/sw_webhook_authentication'
+require 'rack/signalwire_webhook_authentication'
 
-describe Rack::SwWebhookAuthentication do
+describe Rack::SignalwireWebhookAuthentication do
   before do
     @app = ->(_env) { [200, { 'Content-Type' => 'text/plain' }, ['Hello']] }
   end
@@ -10,19 +10,19 @@
   describe 'new' do
     it 'should initialize with an app, auth token and a path' do
       expect do
-        Rack::SwWebhookAuthentication.new(@app, 'ABC', /\/voice/)
+        Rack::SignalwireWebhookAuthentication.new(@app, 'ABC', /\/voice/)
       end.not_to raise_error
     end
 
     it 'should initialize with an app, auth token and paths' do
       expect do
-        Rack::SwWebhookAuthentication.new(@app, 'ABC', /\/voice/, /\/sms/)
+        Rack::SignalwireWebhookAuthentication.new(@app, 'ABC', /\/voice/, /\/sms/)
       end.not_to raise_error
     end
 
     it 'should initialize with an app, dynamic token and paths' do
       expect do
-        Rack::SwWebhookAuthentication.new(@app, nil, /\/voice/, /\/sms/)
+        Rack::SignalwireWebhookAuthentication.new(@app, nil, /\/voice/, /\/sms/)
       end.not_to raise_error
     end
   end
@@ -34,7 +34,7 @@
       expect_any_instance_of(Rack::Request).to receive(:post?).and_return(true)
       expect_any_instance_of(Rack::Request).to receive(:media_type).and_return(Rack::MediaType.type('application/x-www-form-urlencoded'))
       expect_any_instance_of(Rack::Request).to receive(:POST).and_return({ 'AccountSid' => account_sid })
-      @middleware = Rack::SwWebhookAuthentication.new(@app, nil, /\/voice/) { |asid| auth_token }
+      @middleware = Rack::SignalwireWebhookAuthentication.new(@app, nil, /\/voice/) { |asid| auth_token }
       request_validator = double('RequestValidator')
       expect(Twilio::Security::RequestValidator).to receive(:new).with(auth_token).and_return(request_validator)
       expect(request_validator).to receive(:validate).and_return(true)
@@ -46,7 +46,7 @@
 
   describe 'calling against one path' do
     before do
-      @middleware = Rack::SwWebhookAuthentication.new(@app, 'ABC', /\/voice/)
+      @middleware = Rack::SignalwireWebhookAuthentication.new(@app, 'ABC', /\/voice/)
     end
 
     it 'should not intercept when the path doesn\'t match' do
@@ -77,7 +77,7 @@
 
   describe 'calling against many paths' do
     before do
-      @middleware = Rack::SwWebhookAuthentication.new(@app, 'ABC', /\/voice/, /\/sms/)
+      @middleware = Rack::SignalwireWebhookAuthentication.new(@app, 'ABC', /\/voice/, /\/sms/)
     end
 
     it 'should not intercept when the path doesn\'t match' do
@@ -108,7 +108,7 @@
 
   describe 'validating non-form-data POST payloads' do
     it 'should fail if the body does not validate' do
-      middleware = Rack::SwWebhookAuthentication.new(@app, 'qwerty', /\/test/)
+      middleware = Rack::SignalwireWebhookAuthentication.new(@app, 'qwerty', /\/test/)
       input = StringIO.new('{"message": "a post body that does not match the bodySHA256"}')
 
       request = Rack::MockRequest.env_for(
@@ -125,7 +125,7 @@
     end
 
     it 'should validate if the body signature is correct' do
-      middleware = Rack::SwWebhookAuthentication.new(@app, 'qwerty', /\/test/)
+      middleware = Rack::SignalwireWebhookAuthentication.new(@app, 'qwerty', /\/test/)
       input = StringIO.new('{"message": "a post body"}')
 
       request = Rack::MockRequest.env_for(
@@ -142,7 +142,7 @@
     end
 
     it 'should validate even if a previous middleware read the body first' do
-      middleware = Rack::SwWebhookAuthentication.new(@app, 'qwerty', /\/test/)
+      middleware = Rack::SignalwireWebhookAuthentication.new(@app, 'qwerty', /\/test/)
       input = StringIO.new('{"message": "a post body"}')
 
       request = Rack::MockRequest.env_for(
@@ -162,7 +162,7 @@
 
   describe 'validating application/x-www-form-urlencoded POST payloads' do
     it 'should fail if the body does not validate' do
-      middleware = Rack::SwWebhookAuthentication.new(@app, 'qwerty', /\/test/)
+      middleware = Rack::SignalwireWebhookAuthentication.new(@app, 'qwerty', /\/test/)
 
       request = Rack::MockRequest.env_for(
         'https://example.com/test',
@@ -178,7 +178,7 @@
     end
 
     it 'should validate if the body signature is correct' do
-      middleware = Rack::SwWebhookAuthentication.new(@app, 'qwerty', /\/test/)
+      middleware = Rack::SignalwireWebhookAuthentication.new(@app, 'qwerty', /\/test/)
 
       request = Rack::MockRequest.env_for(
         'https://example.com/test',

From a2344fdf1dec2a82e39f2f104021a67e41e95f50 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jo=C3=A3o=20Paulo=20Barbosa=20Marques=20dos=20Santos?=
 <jpsantos@gmail.com>
Date: Thu, 26 Oct 2023 08:19:05 -0300
Subject: [PATCH 4/7] Update lib/rack/signalwire_webhook_authentication.rb

Co-authored-by: Ryan Williams <ryan.williams@signalwire.com>
---
 lib/rack/signalwire_webhook_authentication.rb | 1 -
 1 file changed, 1 deletion(-)

diff --git a/lib/rack/signalwire_webhook_authentication.rb b/lib/rack/signalwire_webhook_authentication.rb
index 1c21263..58bbb1f 100644
--- a/lib/rack/signalwire_webhook_authentication.rb
+++ b/lib/rack/signalwire_webhook_authentication.rb
@@ -5,7 +5,6 @@
 module Rack
 
   class SignalwireWebhookAuthentication
-
     FORM_URLENCODED_MEDIA_TYPE = Rack::MediaType.type('application/x-www-form-urlencoded')
 
     def initialize(app, private_key, *paths, &private_key_lookup)

From 60d429eea629070ccded598a633fd985d7e4318d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jo=C3=A3o=20Paulo=20Barbosa=20Marques=20dos=20Santos?=
 <jpsantos@gmail.com>
Date: Thu, 26 Oct 2023 08:19:15 -0300
Subject: [PATCH 5/7] Update
 spec/rack/signalwire_webhook_authentication_spec.rb

Co-authored-by: Ryan Williams <ryan.williams@signalwire.com>
---
 spec/rack/signalwire_webhook_authentication_spec.rb | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/spec/rack/signalwire_webhook_authentication_spec.rb b/spec/rack/signalwire_webhook_authentication_spec.rb
index 39cd853..1e3d9ef 100644
--- a/spec/rack/signalwire_webhook_authentication_spec.rb
+++ b/spec/rack/signalwire_webhook_authentication_spec.rb
@@ -194,4 +194,4 @@
     end
 
   end
-end
\ No newline at end of file
+end

From af177b94e9c3fb3c31c9548b9ed7ed46a8a0f44c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jo=C3=A3o=20Paulo=20Barbosa=20Marques=20dos=20Santos?=
 <jpsantos@gmail.com>
Date: Thu, 26 Oct 2023 08:19:23 -0300
Subject: [PATCH 6/7] Update lib/rack/signalwire_webhook_authentication.rb

Co-authored-by: Ryan Williams <ryan.williams@signalwire.com>
---
 lib/rack/signalwire_webhook_authentication.rb | 1 -
 1 file changed, 1 deletion(-)

diff --git a/lib/rack/signalwire_webhook_authentication.rb b/lib/rack/signalwire_webhook_authentication.rb
index 58bbb1f..5e26538 100644
--- a/lib/rack/signalwire_webhook_authentication.rb
+++ b/lib/rack/signalwire_webhook_authentication.rb
@@ -3,7 +3,6 @@
 require 'rack/media_type'
 
 module Rack
-
   class SignalwireWebhookAuthentication
     FORM_URLENCODED_MEDIA_TYPE = Rack::MediaType.type('application/x-www-form-urlencoded')
 

From 542db143eca2823fdd55181d07591f2b865b9862 Mon Sep 17 00:00:00 2001
From: Joao Santos <jpsantos@gmail.com>
Date: Thu, 26 Oct 2023 08:23:07 -0300
Subject: [PATCH 7/7] version bump

---
 CHANGELOG.md              | 4 ++++
 lib/signalwire/version.rb | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 157baf9..14d6481 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
 
  ## [Unreleased]
 
+## [2.5.0] - 2023-10-26
+### Added
+- Add Middleware `SignalwireWebhookAuthentication`
+
 ## [2.4.0] - 2023-10-13
 ### Added
 - Add Webhook `ValidateRequest`
diff --git a/lib/signalwire/version.rb b/lib/signalwire/version.rb
index 71abf01..962140d 100644
--- a/lib/signalwire/version.rb
+++ b/lib/signalwire/version.rb
@@ -1,5 +1,5 @@
 # frozen_string_literal: true
 
 module Signalwire
-  VERSION = '2.4.0'
+  VERSION = '2.5.0'
 end