rack-slack_request_verification 0.1.0 → 1.0.0.pre

Sign up to get free protection for your applications and to get access to all the features.
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: