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 +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +4 -0
- data/lib/rack/slack_request_verification.rb +10 -4
- data/lib/rack/slack_request_verification/computed_signature.rb +33 -0
- data/lib/rack/slack_request_verification/configuration.rb +58 -0
- data/lib/rack/slack_request_verification/errors.rb +4 -0
- data/lib/rack/slack_request_verification/headers.rb +39 -0
- data/lib/rack/slack_request_verification/middleware.rb +33 -84
- data/lib/rack/slack_request_verification/request.rb +40 -0
- data/lib/rack/slack_request_verification/version.rb +1 -1
- metadata +8 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b6e156fb4a05f816479e935037c56af58d079f706da577c5d7935cc952195784
|
4
|
+
data.tar.gz: e5f5ccc958eb4458935bfb5ac405381ba7723097aec1206d3287741b4f899f61
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 67664cd0e4d1b6a22c316a2c6189d075de516cad1616cae2f46ca4131c9e6965c82a907b61130915a1e244d50a17dbf5be20499df2d9ce211cf5813f4bbe7d6d
|
7
|
+
data.tar.gz: bfbda6a18fbdd5990e25eb881223b2f45a0538cae8aefe342f1e44290be8b23f515f7138472db16b04789d4a363fb1173e86d67758b1ecbf18b30636a00032de
|
data/Gemfile.lock
CHANGED
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
|
-
|
4
|
-
|
5
|
-
|
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,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
|
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
|
-
@
|
39
|
-
|
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
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
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
|
-
|
67
|
-
|
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
|
-
|
96
|
-
|
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
|
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.
|
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:
|
127
|
+
version: 1.3.1
|
123
128
|
requirements: []
|
124
129
|
rubygems_version: 3.0.3
|
125
130
|
signing_key:
|