Verify Slack Signature By Implementing a Custom Middleware in Sinatra

Verify Slack Signature By Implementing a Custom Middleware in Sinatra

Middleware in a Sinatra application intercepts, processes, and modifies HTTP requests and responses. It is useful for implementing features like authentication, logging, caching, and other cross-cutting concerns that apply to multiple routes in your application.

In this blog post, we will build a custom middleware to validate requests from the Slack Events API to our Ruby Sinatra server.

The Context

The high-level context

The background is that we have configured our server to receive an event when someone messages our Slack bot directly. We use the Slack Events API for this purpose.

The Slack events API accepts a webhook, which is our server’s endpoint. Slack will post requests with the body payload to our endpoint. The request body will contain information about the message sent through Slack. On receiving this, we can process the payload and decide how we want our bot to respond to the user.

In our specific case this setup was built for our experimental Slack chatbot powered by an LLM:

We won’t get into the details of the server logic or the bot itself, but will only focus on writing custom middleware. We’ll do that by walking through how we implemented a validation system for the requests we receive from Slack.


What is a Middleware, Anyway?

Middleware in Rack-based frameworks like Sinatra acts as a pipeline for processing HTTP requests and responses. Sinatra, being built on Rack, allows the use of Rack middleware directly. Middleware operates in a linear chain:

  • A request comes in.
  • It passes through the middleware chain.
  • Each middleware component can process or modify the request, and then decide whether to pass it down the chain or stop the process.

Rack is a modular interface between web servers and web applications.?

You can find more about Rack, from its GitHub repository.

Since Sinatra is a framework built on top of Rack, we can design our custom Rack middleware and integrate it with Sinatra seamlessly.


Creating a Custom Middleware

A middleware is a Ruby class that follows a specific structure. It needs to implement an initialize method and a call method. The call method receives the Rack environment hash and returns an array of three elements: the HTTP status, headers, and body.

The structure of a request middleware is as follows:

class CustomMiddleware
  def initialize(app)
    @app = app
  end

  def call(env)
    # middleware logic goes here. 

    # allow to continue to our app logic 
    @app.call(env)
  end
end        


The Slack Signature Verification Middleware

Let’s build the Slack signature verification middleware:

class SlackSignatureVerification
  def initialize(app)
    @app = app
  end

  def call(env)
    @app.call(env)
  end
end        

We need to check the signature and timestamp from the Slack requests and reject the request on mismatch. If all looks good, we allow the request to flow into our application logic.

Let’s build it step by step:

class SlackSignatureVerification
  def initialize(app)
    @app = app
  end

  def call(env)
    request = Rack::Request.new(env)
    response = JSON.parse verify_signature(request)
    unless response['is_success']
      return [response['status_code'], { 'Content-Type' => 'application/json' },
              [response['message']]]
    end

    @app.call(env)
  end

  private
  
  def verify_signature(request)
    {
      is_success: true
    }.to_json
  end
end        

For now, the `verify_signature` method passes everything a success. Let’s implement the logic to actually verify the signature:

Verification Logic?

  1. Timestamp verification

To prevent replay attacks, we’ll reject any request that is older than 5 minutes in comparison to the local server time:

class SlackSignatureVerification
  # ... existing code

  private
  
  def verify_signature(request)
    timestamp = request.env['HTTP_X_SLACK_REQUEST_TIMESTAMP']

     if timestamp.nil? || timestamp.to_i < Time.now.to_i - 60 * 5
        return {
          is_success: false,
          status_code: 400,
          message: 'Invalid timestamp'
        }.to_json
     end

    {
      is_success: true
    }.to_json

   end
end        

2. Signature Verification

The `X_SLACK_SIGNATURE` header we receive from the Slack requests is an HMAC (Hashed Message Access Code). This means we will need a secret key from Slack to recompute the HMAC.?

Since the raw body is used as a part of the HMAC computation, it ensures that the request body hasn’t been tampered with and also ensures that the request originated from Slack servers.

We will get the signing secret from the Slack Bot App dashboard and store it as an environment variable. We will load that variable into our code using the `dotenv` gem.

class SlackSignatureVerification
  # ... existing code

  private
  
  def verify_signature(request)
    timestamp = request.env['HTTP_X_SLACK_REQUEST_TIMESTAMP']
    slack_signature = request.env['HTTP_X_SLACK_SIGNATURE']
    body = request.body.read

    if timestamp.nil? || slack_signature.nil? || timestamp.to_i < Time.now.to_i - 60 * 5
       return {
         is_success: false,
         status_code: 400,
         message: 'Unknown signature or timestamp'
       }.to_json
    end

    sig_basestring = "v0:#{timestamp}:#{body}"
    computed_signature = "v0=#{OpenSSL::HMAC.hexdigest('SHA256', ENV['SLACK_SIGNING_SECRET'], sig_basestring)}"
    is_valid_signature = Rack::Utils.secure_compare(computed_signature, slack_signature)

    unless is_valid_signature
      return {
        is_success: false,
        status_code: 403,
        message: 'Invalid signature'
      }.to_json
    end
    
    {
      is_success: true
    }.to_json

   end
end        
It is also crucial to note that we use the `secure_compare` method to compare the signatures. This ensures the strings are compared in constant time irrespective of the string content. It is a defence against the side-channel timing attack, where an attacker tries to gain information about the system by measuring the time it takes to execute cryptographic algorithms.


Unit Testing the Middleware

We can use the rack/test gem and Ruby’s minitest to quickly write a unit test to verify our implementation as follows:

require 'rack'
require 'rack/test'
require 'minitest/autorun'
require 'openssl'
require 'json'
require 'dotenv/load'

require_relative '../../../middlewares/slack_signature_verification'

class SlackSignatureVerificationTest < Minitest::Test
  include Rack::Test::Methods

  def app
    Rack::Builder.new do
      use SlackSignatureVerification
      run ->(env) { [200, { 'Content-Type' => 'application/json' }, ['OK']] }
    end
  end

  def test_valid_signature
    timestamp = Time.now.to_i.to_s
    body = 'test body'
    sig_basestring = "v0:#{timestamp}:#{body}"
    slack_signature = "v0=#{OpenSSL::HMAC.hexdigest('SHA256', ENV['SLACK_SIGNING_SECRET'], sig_basestring)}"

    header 'X-Slack-Request-Timestamp', timestamp
    header 'X-Slack-Signature', slack_signature
    post '/', body

    assert last_response.ok?
    assert_equal 'OK', last_response.body
  end

  def test_invalid_signature
    header 'X-Slack-Request-Timestamp', Time.now.to_i.to_s
    header 'X-Slack-Signature', 'invalid_signature'
    post '/', 'test body'

    assert_equal 403, last_response.status
    assert_equal 'Invalid signature', last_response.body
  end

  def test_missing_signature
    post '/', 'test body'

    assert_equal 400, last_response.status
    assert_equal 'Unknown signature or timestamp', last_response.body
  end
end        

The following command could be used to run the tests:

bundle exec ruby -Itest tests/unit/middlewares/slack_signature_verification_test.rb        
Unit test run


Adding the Middleware to the Sinatra Application

Now that we have our custom middleware implemented and unit tested, we can integrate it with our server by requiring and using it in the application root file, which is usually named server.rb?:

# frozen_string_literal: true

require 'sinatra'
require 'json'
require 'dotenv/load'


require_relative 'middlewares/slack_signature_verification'


use SlackSignatureVerification 

set :port, ENV['PORT']
set :environment, :production


post '/chat' do
  # server logic here
end        

And that's it. Now, all our requests will be validated by verifying the Slack signature header ??.


The article was originally published on my Medium blog.


Parthipan N.

Full-stack Developer | JavaScript | TypeScript | Python | MERN | AWS | PostgreSQL

2 个月

要查看或添加评论,请登录

Parthipan N.的更多文章

社区洞察

其他会员也浏览了