Verify Slack Signature By Implementing a Custom Middleware in Sinatra
Parthipan N.
Full-stack Developer | JavaScript | TypeScript | Python | MERN | AWS | PostgreSQL
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 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:
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?
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
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.
Full-stack Developer | JavaScript | TypeScript | Python | MERN | AWS | PostgreSQL
2 个月Medium blog link: https://medium.com/gitconnected/creating-custom-middleware-for-a-sinatra-application-in-ruby-07d743b9c4bd