rack-slack_request_verification 0.1.0 → 1.0.0.pre

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 62381da5d7fb58f307747f07e786e8ab2146a56128e18edcdff142ae007a2896
4
- data.tar.gz: cbad6d1cf50520166b7e82e6a9ff13b1346b78a1e5cb55f06d43317b4bab9b93
3
+ metadata.gz: b6e156fb4a05f816479e935037c56af58d079f706da577c5d7935cc952195784
4
+ data.tar.gz: e5f5ccc958eb4458935bfb5ac405381ba7723097aec1206d3287741b4f899f61
5
5
  SHA512:
6
- metadata.gz: 5c9884a7729394cfefcf3dba9550a57d3c92986d1b3e7dba68fa844ef58c3d8105cd005d035fa5f0e29405e7b89221eaf92e5fa6188de574e5d04f64546fe5e4
7
- data.tar.gz: 6aa11e0045975efefab53246b074dca2f962cc28c2821ec6743935c76cab7e8c1a4561c5dfbeb00c61944c9da57434a6bb8caf5dae10c0d49c15e169b6498608
6
+ metadata.gz: 67664cd0e4d1b6a22c316a2c6189d075de516cad1616cae2f46ca4131c9e6965c82a907b61130915a1e244d50a17dbf5be20499df2d9ce211cf5813f4bbe7d6d
7
+ data.tar.gz: bfbda6a18fbdd5990e25eb881223b2f45a0538cae8aefe342f1e44290be8b23f515f7138472db16b04789d4a363fb1173e86d67758b1ecbf18b30636a00032de
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rack-slack_request_verification (0.1.0)
4
+ rack-slack_request_verification (1.0.0.pre)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -61,6 +61,10 @@ use Rack::SlackRequestVerification, {
61
61
  # process each request once
62
62
  max_staleness_in_secs: 60 * 5,
63
63
 
64
+ # The entire request body must be loaded into memory to compute the hash.
65
+ # To prevent a DDoS attack, the request body is limited to 1MB
66
+ request_body_limit_in_bytes: 1024 ** 2,
67
+
64
68
  # Where to log error messages
65
69
  logger: Logger.new($stdout),
66
70
 
@@ -1,12 +1,18 @@
1
+ require 'forwardable'
2
+
1
3
  module Rack
2
4
  module SlackRequestVerification
3
- class Error < StandardError; end
4
-
5
- def self.new(*args)
6
- Middleware.new(*args)
5
+ def self.new(app, *args)
6
+ config = Configuration.new(*args)
7
+ Middleware.new(app, config)
7
8
  end
8
9
  end
9
10
  end
10
11
 
11
12
  require "rack/slack_request_verification/version"
13
+ require "rack/slack_request_verification/errors"
12
14
  require "rack/slack_request_verification/middleware"
15
+ require "rack/slack_request_verification/configuration"
16
+ require "rack/slack_request_verification/request"
17
+ require "rack/slack_request_verification/headers"
18
+ require "rack/slack_request_verification/computed_signature"
@@ -0,0 +1,33 @@
1
+ require 'openssl'
2
+ require 'forwardable'
3
+
4
+ module Rack::SlackRequestVerification
5
+ class ComputedSignature
6
+ extend Forwardable
7
+ def_delegators :@config, :signing_key, :signing_version
8
+ def_delegators :@request, :body, :timestamp
9
+
10
+ def initialize(request)
11
+ @request = request
12
+ @config = request.config
13
+ end
14
+
15
+ def to_s
16
+ [signing_version, digest].join('=')
17
+ end
18
+
19
+ def ==(other)
20
+ other == to_s
21
+ end
22
+
23
+ private
24
+
25
+ def signature_base_string
26
+ [signing_version, timestamp, body].join(':')
27
+ end
28
+
29
+ def digest
30
+ OpenSSL::HMAC.hexdigest("SHA256", signing_key, signature_base_string)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,58 @@
1
+ require 'logger'
2
+
3
+ module Rack::SlackRequestVerification
4
+ class Configuration
5
+ attr_reader *%i(
6
+ signing_key
7
+ path_pattern
8
+ signing_version
9
+ timestamp_header
10
+ signature_header
11
+ logger
12
+ max_staleness_in_secs
13
+ request_body_limit_in_bytes
14
+ )
15
+
16
+ def initialize(
17
+ # A regular expression used to determine which requests to verify
18
+ path_pattern:,
19
+
20
+ # You can provide a signing key directly, set a SLACK_SIGNING_KEY env var
21
+ # or customise the env var to something else
22
+ signing_key: nil,
23
+ signing_key_env_var: 'SLACK_SIGNING_KEY',
24
+
25
+ # Mitigates replay attacks by verifying the request was sent recently –
26
+ # a better strategy is to record the signature header to ensure you only
27
+ # process each request once
28
+ max_staleness_in_secs: 60 * 5,
29
+
30
+ # The entire request body must be loaded into memory to compute the hash.
31
+ # To prevent a DDoS attack, the request body is limited to 1MB
32
+ request_body_limit_in_bytes: 1024 ** 2,
33
+
34
+ # Where to log error messages
35
+ logger: Logger.new($stdout),
36
+
37
+ signing_version: 'v0',
38
+ timestamp_header: 'X-Slack-Request-Timestamp',
39
+ signature_header: 'X-Slack-Signature'
40
+ )
41
+ @path_pattern = path_pattern
42
+ @signing_version = signing_version
43
+ @timestamp_header = timestamp_header
44
+ @signature_header = signature_header
45
+ @logger = logger
46
+ @max_staleness_in_secs = max_staleness_in_secs
47
+ @request_body_limit_in_bytes = request_body_limit_in_bytes
48
+
49
+ @signing_key = signing_key || ENV.fetch(signing_key_env_var) do
50
+ fail Error, "#{signing_key_env_var} env var not set, please configure a signing key"
51
+ end
52
+ end
53
+
54
+ def minimum_timestamp
55
+ Time.now.to_i - max_staleness_in_secs
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,4 @@
1
+ module Rack::SlackRequestVerification
2
+ Error = Class.new(StandardError)
3
+ RequestBodyTooLarge = Class.new(Error)
4
+ end
@@ -0,0 +1,39 @@
1
+ module Rack::SlackRequestVerification
2
+ class Headers
3
+ extend Forwardable
4
+ def_delegators :@config, :signature_header, :timestamp_header
5
+ attr_reader :request, :to_h
6
+
7
+ def initialize(request)
8
+ @request = request
9
+ @config = request.config
10
+
11
+ @to_h = {
12
+ signature_header => read(signature_header),
13
+ timestamp_header => read(timestamp_header)&.to_i
14
+ }
15
+ end
16
+
17
+ def signed_signature
18
+ to_h.fetch(signature_header)
19
+ end
20
+
21
+ def timestamp
22
+ to_h.fetch(timestamp_header)
23
+ end
24
+
25
+ def missing
26
+ to_h.select { |_, value| value.nil? }.keys
27
+ end
28
+
29
+ def missing?
30
+ !missing.empty?
31
+ end
32
+
33
+ private
34
+
35
+ def read(header)
36
+ request.env["HTTP_" + header.gsub('-', '_').upcase]
37
+ end
38
+ end
39
+ end
@@ -1,99 +1,48 @@
1
1
  require 'logger'
2
2
  require 'openssl'
3
+ require 'forwardable'
3
4
 
4
5
  module Rack::SlackRequestVerification
5
- class Middleware
6
- attr_reader *%i(
7
- signing_key
8
- path_pattern
9
- signing_version
10
- timestamp_header
11
- signature_header
12
- logger
13
- max_staleness_in_secs
14
- )
6
+ class Middleware < SimpleDelegator
7
+ attr_reader :app, :config
15
8
 
16
- def initialize(app,
17
- # A regular expression used to determine which requests to verify
18
- path_pattern:,
19
-
20
- # You can provide a signing key directly, set a SLACK_SIGNING_KEY env var
21
- # or customise the env var to something else
22
- signing_key: nil,
23
- signing_key_env_var: 'SLACK_SIGNING_KEY',
24
-
25
- # Mitigates replay attacks by verifying the request was sent recently –
26
- # a better strategy is to record the signature header to ensure you only
27
- # process each request once
28
- max_staleness_in_secs: 60 * 5,
29
-
30
- # Where to log error messages
31
- logger: Logger.new($stdout),
32
-
33
- signing_version: 'v0',
34
- timestamp_header: 'X-Slack-Request-Timestamp',
35
- signature_header: 'X-Slack-Signature'
36
- )
9
+ def initialize(app, config)
37
10
  @app = app
38
- @path_pattern = path_pattern
39
- @signing_version = signing_version
40
- @timestamp_header = timestamp_header
41
- @signature_header = signature_header
42
- @logger = logger
43
- @max_staleness_in_secs = max_staleness_in_secs
44
-
45
- @signing_key = signing_key || ENV.fetch(signing_key_env_var) do
46
- fail Error, "#{signing_key_env_var} env var not set, please configure a signing key"
47
- end
11
+ @config = config
12
+ super(config)
48
13
  end
49
14
 
50
15
  def call(env)
51
- if !path_pattern.match?(env['PATH_INFO'])
52
- @app.call(env)
53
- else
54
- headers = {
55
- signature_header => env["HTTP_" + signature_header.gsub('-', '_').upcase],
56
- timestamp_header => env["HTTP_" + timestamp_header.gsub('-', '_').upcase]&.to_i
57
- }
58
-
59
- missing_headers = headers.select { |_, value| value.nil? }.keys
60
-
61
- if !missing_headers.empty?
62
- logger.error "Slack verification failed: missing #{missing_headers.join(', ')}"
63
- return [401, {}, "Not authorized"]
16
+ request = Request.new(env, config)
17
+
18
+ begin
19
+ if path_pattern.match?(request.path)
20
+ if request.headers.missing?
21
+ logger.error "Slack verification failed: missing #{request.headers.missing.join(', ')}"
22
+ return respond_with(401)
23
+ end
24
+
25
+ if request.timestamp < minimum_timestamp
26
+ logger.error "Slack verification failed: #{timestamp_header} is #{request.timestamp}, only #{minimum_timestamp} or later is allowed"
27
+ return respond_with(401)
28
+ end
29
+
30
+ if request.computed_signature != request.signed_signature
31
+ logger.error "Slack verification failed: #{signature_header} does not match the signature"
32
+ return respond_with(401)
33
+ end
64
34
  end
35
+ rescue RequestBodyTooLarge
36
+ logger.error "Slack verification failed: request exceeded limit of #{request_body_limit_in_bytes} bytes"
37
+ return respond_with(413)
38
+ end
65
39
 
66
- timestamp = headers[timestamp_header]
67
- signature = headers[signature_header]
68
-
69
- minimum_timestamp = Time.now.to_i - max_staleness_in_secs
70
-
71
- if timestamp < minimum_timestamp
72
- logger.error "Slack verification failed: #{timestamp_header} is #{timestamp}, only #{minimum_timestamp} or later is allowed"
73
- return [401, {}, "Not authorized"]
74
- end
75
-
76
- body = env['rack.input']
77
-
78
- signature_base_string = [
79
- signing_version,
80
- timestamp,
81
- body.read
82
- ].join(':')
83
-
84
- body.rewind
85
-
86
- digest = OpenSSL::HMAC.hexdigest("SHA256", signing_key, signature_base_string)
87
-
88
- computed_signature = [signing_version, digest].join('=')
89
-
90
- if computed_signature != signature
91
- logger.error "Slack verification failed: #{signature_header} does not match the signature"
92
- return [401, {}, "Not authorized"]
93
- end
40
+ app.call(env)
41
+ end
94
42
 
95
- @app.call(env)
96
- end
43
+ def respond_with(code)
44
+ body = Rack::Utils::HTTP_STATUS_CODES.fetch(code)
45
+ [code, {}, [body]]
97
46
  end
98
47
  end
99
48
  end
@@ -0,0 +1,40 @@
1
+ module Rack::SlackRequestVerification
2
+ class Request
3
+ extend Forwardable
4
+ attr_reader :env, :config
5
+ def_delegators :headers, :signed_signature, :timestamp
6
+ def_delegators :config, :request_body_limit_in_bytes
7
+
8
+ def initialize(env, config)
9
+ @env = env
10
+ @config = config
11
+ end
12
+
13
+ def path
14
+ env.fetch('PATH_INFO')
15
+ end
16
+
17
+ def headers
18
+ @headers ||= Headers.new(self)
19
+ end
20
+
21
+ def body
22
+ input_rack_io = env.fetch('rack.input')
23
+ bytes = input_rack_io.read(request_body_limit_in_bytes)
24
+
25
+ # Attempt to read one more byte
26
+ reading_is_complete = input_rack_io.read(1).nil?
27
+
28
+ # Rewind for the next middleware
29
+ input_rack_io.rewind
30
+
31
+ fail RequestBodyTooLarge unless reading_is_complete
32
+
33
+ bytes
34
+ end
35
+
36
+ def computed_signature
37
+ @computed_signature ||= ComputedSignature.new(self)
38
+ end
39
+ end
40
+ end
@@ -1,5 +1,5 @@
1
1
  module Rack
2
2
  module SlackRequestVerification
3
- VERSION = "0.1.0"
3
+ VERSION = "1.0.0.pre"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rack-slack_request_verification
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 1.0.0.pre
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pete Nicholls
@@ -99,7 +99,12 @@ files:
99
99
  - bin/console
100
100
  - bin/setup
101
101
  - lib/rack/slack_request_verification.rb
102
+ - lib/rack/slack_request_verification/computed_signature.rb
103
+ - lib/rack/slack_request_verification/configuration.rb
104
+ - lib/rack/slack_request_verification/errors.rb
105
+ - lib/rack/slack_request_verification/headers.rb
102
106
  - lib/rack/slack_request_verification/middleware.rb
107
+ - lib/rack/slack_request_verification/request.rb
103
108
  - lib/rack/slack_request_verification/version.rb
104
109
  - rack-slack_request_verification.gemspec
105
110
  homepage: https://github.com/Aupajo/rack-slack_request_verification
@@ -117,9 +122,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
117
122
  version: '0'
118
123
  required_rubygems_version: !ruby/object:Gem::Requirement
119
124
  requirements:
120
- - - ">="
125
+ - - ">"
121
126
  - !ruby/object:Gem::Version
122
- version: '0'
127
+ version: 1.3.1
123
128
  requirements: []
124
129
  rubygems_version: 3.0.3
125
130
  signing_key: