pusher-signature 0.1.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d5da8362f6b4bc56675ed04e37fd83224c0c559e
4
+ data.tar.gz: 998d3fe87151b510e506dc169217be6c9ead9d96
5
+ SHA512:
6
+ metadata.gz: e978ecc1590582051b005131501bb3a023eaaff387409ff04d63b4290b59b6c158d2bda092da0da22d3336d97204f96b25b8bb42c84872a369df5a044d2f4988
7
+ data.tar.gz: 65d3754c8ed90c2dd0bcb9945215ed07a60777f95ac8f01aeae6a09f5de56393f2026ec5b74389b02a3e33e9b236fb5762ac0ec1c5f6287b841b683ec00ddd21
@@ -0,0 +1,23 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
22
+ .rbx
23
+ .rspec
@@ -0,0 +1,18 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.8.7
4
+ - 1.9.2
5
+ - 1.9.3
6
+ - 2.0.0
7
+ - jruby-18mode
8
+ - jruby-19mode
9
+ - rbx-18mode
10
+ - rbx-19mode
11
+ matrix:
12
+ allow_failures:
13
+ - rvm: jruby-18mode
14
+ - rvm: jruby-19mode
15
+ - rvm: rbx-18mode
16
+ - rvm: rbx-19mode
17
+
18
+ script: bundle exec rspec spec
@@ -0,0 +1,5 @@
1
+
2
+ 0.1.8 / 2015-01-16
3
+ ==================
4
+
5
+ * SECURITY: Perform constant time string comparison when validating signatures
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
@@ -0,0 +1,35 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ pusher-signature (0.1.8)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ bacon (1.2.0)
10
+ diff-lcs (1.2.5)
11
+ em-spec (0.2.6)
12
+ bacon
13
+ eventmachine
14
+ rspec (> 2.6.0)
15
+ test-unit
16
+ eventmachine (1.0.8)
17
+ power_assert (0.2.4)
18
+ rspec (2.13.0)
19
+ rspec-core (~> 2.13.0)
20
+ rspec-expectations (~> 2.13.0)
21
+ rspec-mocks (~> 2.13.0)
22
+ rspec-core (2.13.1)
23
+ rspec-expectations (2.13.0)
24
+ diff-lcs (>= 1.1.3, < 2.0)
25
+ rspec-mocks (2.13.1)
26
+ test-unit (3.1.4)
27
+ power_assert
28
+
29
+ PLATFORMS
30
+ ruby
31
+
32
+ DEPENDENCIES
33
+ em-spec (= 0.2.6)
34
+ pusher-signature!
35
+ rspec (= 2.13.0)
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Martyn Loughran, 2015 Pusher Ltd
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,67 @@
1
+ pusher-signature
2
+ =========
3
+
4
+ [![Build Status](https://secure.travis-ci.org/pusher/pusher-signature.png?branch=master)](http://travis-ci.org/pusher/pusher-signature)
5
+
6
+ This gem is a fork of the [signature](https://github.com/mloughran/signature) gem, written by [Martyn Loughran](https://github.com/mloughran). It has now been released as a separate gem to resolve namespace collisions.
7
+
8
+ Examples
9
+ --------
10
+
11
+ Client example
12
+
13
+ ```ruby
14
+ params = {:some => 'parameters'}
15
+ token = Pusher::Signature::Token.new('my_key', 'my_secret')
16
+ request = Pusher::Signature::Request.new('POST', '/api/thing', params)
17
+ auth_hash = request.sign(token)
18
+ query_params = params.merge(auth_hash)
19
+
20
+ HTTParty.post('http://myservice/api/thing', {
21
+ :body => query_params
22
+ })
23
+ ```
24
+
25
+ `query_params` looks like:
26
+
27
+ ```ruby
28
+ {
29
+ :some => "parameters",
30
+ :auth_timestamp => 1273231888,
31
+ :auth_signature => "28b6bb0f242f71064916fad6ae463fe91f5adc302222dfc02c348ae1941eaf80",
32
+ :auth_version => "1.0",
33
+ :auth_key => "my_key"
34
+ }
35
+
36
+ ```
37
+ Server example (sinatra)
38
+
39
+ ```ruby
40
+ error Pusher::Signature::AuthenticationError do |controller|
41
+ error = controller.env["sinatra.error"]
42
+ halt 401, "401 UNAUTHORIZED: #{error.message}\n"
43
+ end
44
+
45
+ post '/api/thing' do
46
+ request = Pusher::Signature::Request.new('POST', env["REQUEST_PATH"], params)
47
+ # This will raise a Signature::AuthenticationError if request does not authenticate
48
+ token = request.authenticate do |key|
49
+ Pusher::Signature::Token.new(key, lookup_secret(key))
50
+ end
51
+
52
+ # Do whatever you need to do
53
+ end
54
+ ```
55
+
56
+ Developing
57
+ ----------
58
+
59
+ bundle
60
+ bundle exec rspec spec/*_spec.rb
61
+
62
+ Please see the travis status for a list of rubies tested against
63
+
64
+ Copyright
65
+ ---------
66
+
67
+ Copyright (c) 2010 Martyn Loughran, 2015 Pusher Ltd. See LICENSE for details.
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
@@ -0,0 +1 @@
1
+ require 'pusher/signature'
@@ -0,0 +1,234 @@
1
+ require 'openssl'
2
+
3
+ require 'pusher/signature/query_encoder'
4
+
5
+ module Pusher
6
+ module Signature
7
+ class AuthenticationError < RuntimeError; end
8
+
9
+ class Token
10
+ attr_reader :key, :secret
11
+
12
+ def initialize(key, secret)
13
+ @key, @secret = key, secret
14
+ end
15
+
16
+ def sign(request)
17
+ request.sign(self)
18
+ end
19
+ end
20
+
21
+ class Request
22
+ attr_accessor :path, :query_hash
23
+
24
+ include QueryEncoder
25
+
26
+ # http://www.w3.org/TR/NOTE-datetime
27
+ ISO8601 = "%Y-%m-%dT%H:%M:%SZ"
28
+
29
+ def initialize(method, path, query)
30
+ raise ArgumentError, "Expected string" unless path.kind_of?(String)
31
+ raise ArgumentError, "Expected hash" unless query.kind_of?(Hash)
32
+
33
+ query_hash = {}
34
+ auth_hash = {}
35
+ query.each do |key, v|
36
+ k = key.to_s.downcase
37
+ k[0..4] == 'auth_' ? auth_hash[k] = v : query_hash[k] = v
38
+ end
39
+
40
+ @method = method.upcase
41
+ @path, @query_hash, @auth_hash = path, query_hash, auth_hash
42
+ @signed = false
43
+ end
44
+
45
+ # Sign the request with the given token, and return the computed
46
+ # authentication parameters
47
+ #
48
+ def sign(token)
49
+ @auth_hash = {
50
+ :auth_version => "1.0",
51
+ :auth_key => token.key,
52
+ :auth_timestamp => Time.now.to_i.to_s
53
+ }
54
+ @auth_hash[:auth_signature] = signature(token)
55
+
56
+ @signed = true
57
+
58
+ return @auth_hash
59
+ end
60
+
61
+ # Authenticates the request with a token
62
+ #
63
+ # Raises an AuthenticationError if the request is invalid.
64
+ # AuthenticationError exception messages are designed to be exposed to API
65
+ # consumers, and should help them correct errors generating signatures
66
+ #
67
+ # Timestamp: Unless timestamp_grace is set to nil (which allows this check
68
+ # to be skipped), AuthenticationError will be raised if the timestamp is
69
+ # missing or further than timestamp_grace period away from the real time
70
+ # (defaults to 10 minutes)
71
+ #
72
+ # Signature: Raises AuthenticationError if the signature does not match
73
+ # the computed HMAC. The error contains a hint for how to sign.
74
+ #
75
+ def authenticate_by_token!(token, timestamp_grace = 600)
76
+ # Validate that your code has provided a valid token. This does not
77
+ # raise an AuthenticationError since passing tokens with empty secret is
78
+ # a code error which should be fixed, not reported to the API's consumer
79
+ if token.secret.nil? || token.secret.empty?
80
+ raise "Provided token is missing secret"
81
+ end
82
+
83
+ validate_version!
84
+ validate_timestamp!(timestamp_grace)
85
+ validate_signature!(token)
86
+ true
87
+ end
88
+
89
+ # Authenticate the request with a token, but rather than raising an
90
+ # exception if the request is invalid, simply returns false
91
+ #
92
+ def authenticate_by_token(token, timestamp_grace = 600)
93
+ authenticate_by_token!(token, timestamp_grace)
94
+ rescue AuthenticationError
95
+ false
96
+ end
97
+
98
+ # Authenticate a request
99
+ #
100
+ # Takes a block which will be called with the auth_key from the request,
101
+ # and which should return a Signature::Token (or nil if no token can be
102
+ # found for the key)
103
+ #
104
+ # Raises errors in the same way as authenticate_by_token!
105
+ #
106
+ def authenticate(timestamp_grace = 600)
107
+ raise ArgumentError, "Block required" unless block_given?
108
+ key = @auth_hash['auth_key']
109
+ raise AuthenticationError, "Missing parameter: auth_key" unless key
110
+ token = yield key
111
+ unless token
112
+ raise AuthenticationError, "Unknown auth_key"
113
+ end
114
+ authenticate_by_token!(token, timestamp_grace)
115
+ return token
116
+ end
117
+
118
+ # Authenticate a request asynchronously
119
+ #
120
+ # This method is useful it you're running a server inside eventmachine and
121
+ # need to lookup the token asynchronously.
122
+ #
123
+ # The block is passed an auth key and a deferrable which should succeed
124
+ # with the token, or fail if the token cannot be found
125
+ #
126
+ # This method returns a deferrable which succeeds with the valid token, or
127
+ # fails with an AuthenticationError which can be used to pass the error
128
+ # back to the user
129
+ #
130
+ def authenticate_async(timestamp_grace = 600)
131
+ raise ArgumentError, "Block required" unless block_given?
132
+ df = EM::DefaultDeferrable.new
133
+
134
+ key = @auth_hash['auth_key']
135
+
136
+ unless key
137
+ df.fail(AuthenticationError.new("Missing parameter: auth_key"))
138
+ return
139
+ end
140
+
141
+ token_df = yield key
142
+ token_df.callback { |token|
143
+ begin
144
+ authenticate_by_token!(token, timestamp_grace)
145
+ df.succeed(token)
146
+ rescue AuthenticationError => e
147
+ df.fail(e)
148
+ end
149
+ }
150
+ token_df.errback {
151
+ df.fail(AuthenticationError.new("Unknown auth_key"))
152
+ }
153
+ ensure
154
+ return df
155
+ end
156
+
157
+ # Expose the authentication parameters for a signed request
158
+ #
159
+ def auth_hash
160
+ raise "Request not signed" unless @signed
161
+ @auth_hash
162
+ end
163
+
164
+ # Query parameters merged with the computed authentication parameters
165
+ #
166
+ def signed_params
167
+ @query_hash.merge(auth_hash)
168
+ end
169
+
170
+ private
171
+
172
+ def signature(token)
173
+ digest = OpenSSL::Digest::SHA256.new
174
+ OpenSSL::HMAC.hexdigest(digest, token.secret, string_to_sign)
175
+ end
176
+
177
+ def string_to_sign
178
+ [@method, @path, parameter_string].join("\n")
179
+ end
180
+
181
+ def parameter_string
182
+ param_hash = @query_hash.merge(@auth_hash || {})
183
+
184
+ # Convert keys to lowercase strings
185
+ hash = {}; param_hash.each { |k,v| hash[k.to_s.downcase] = v }
186
+
187
+ # Exclude signature from signature generation!
188
+ hash.delete("auth_signature")
189
+
190
+ hash.sort.map do |k, v|
191
+ QueryEncoder.encode_param_without_escaping(k, v)
192
+ end.join('&')
193
+ end
194
+
195
+ def validate_version!
196
+ version = @auth_hash["auth_version"]
197
+ raise AuthenticationError, "Version required" unless version
198
+ raise AuthenticationError, "Version not supported" unless version == '1.0'
199
+ end
200
+
201
+ def validate_timestamp!(grace)
202
+ return true if grace.nil?
203
+
204
+ timestamp = @auth_hash["auth_timestamp"]
205
+ error = (timestamp.to_i - Time.now.to_i).abs
206
+ raise AuthenticationError, "Timestamp required" unless timestamp
207
+ if error >= grace
208
+ raise AuthenticationError, "Timestamp expired: Given timestamp "\
209
+ "(#{Time.at(timestamp.to_i).utc.strftime(ISO8601)}) "\
210
+ "not within #{grace}s of server time "\
211
+ "(#{Time.now.utc.strftime(ISO8601)})"
212
+ end
213
+ return true
214
+ end
215
+
216
+ def validate_signature!(token)
217
+ unless identical? @auth_hash["auth_signature"], signature(token)
218
+ raise AuthenticationError, "Invalid signature: you should have "\
219
+ "sent HmacSHA256Hex(#{string_to_sign.inspect}, your_secret_key)"\
220
+ ", but you sent #{@auth_hash["auth_signature"].inspect}"
221
+ end
222
+ return true
223
+ end
224
+
225
+ # Constant time string comparison
226
+ def identical?(a, b)
227
+ return true if a.nil? && b.nil?
228
+ return false if a.nil? || b.nil?
229
+ return false unless a.bytesize == b.bytesize
230
+ a.bytes.zip(b.bytes).reduce(0) { |memo, (a, b)| memo += a ^ b } == 0
231
+ end
232
+ end
233
+ end
234
+ end
@@ -0,0 +1,49 @@
1
+ module Pusher
2
+ module Signature
3
+ # Query string encoding extracted with thanks from em-http-request
4
+ module QueryEncoder
5
+ class << self
6
+ # URL encodes query parameters:
7
+ # single k=v, or a URL encoded array, if v is an array of values
8
+ def encode_param(k, v)
9
+ if v.is_a?(Array)
10
+ v.map { |e| escape(k) + "[]=" + escape(e) }.join("&")
11
+ else
12
+ escape(k) + "=" + escape(v)
13
+ end
14
+ end
15
+
16
+ # Like encode_param, but doesn't url escape keys or values
17
+ def encode_param_without_escaping(k, v)
18
+ if v.is_a?(Array)
19
+ v.map { |e| k + "[]=" + e }.join("&")
20
+ else
21
+ "#{k}=#{v}"
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def escape(s)
28
+ if defined?(EscapeUtils)
29
+ EscapeUtils.escape_url(s.to_s)
30
+ else
31
+ s.to_s.gsub(/([^a-zA-Z0-9_.-]+)/n) {
32
+ '%'+$1.unpack('H2'*bytesize($1)).join('%').upcase
33
+ }
34
+ end
35
+ end
36
+
37
+ if ''.respond_to?(:bytesize)
38
+ def bytesize(string)
39
+ string.bytesize
40
+ end
41
+ else
42
+ def bytesize(string)
43
+ string.size
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,5 @@
1
+ module Pusher
2
+ module Signature
3
+ VERSION = "0.1.8"
4
+ end
5
+ end
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "pusher/signature/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "pusher-signature"
7
+ s.version = Pusher::Signature::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Martyn Loughran", "Pusher Ltd"]
10
+ s.email = ["me@mloughran.com", "support@pusher.com"]
11
+ s.homepage = "http://github.com/pusher/pusher-signature"
12
+ s.summary = %q{Simple key/secret based authentication for apis}
13
+ s.description = %q{Simple key/secret based authentication for apis}
14
+
15
+ s.files = `git ls-files`.split("\n")
16
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18
+ s.require_paths = ["lib"]
19
+ s.license = 'MIT'
20
+
21
+ s.add_dependency "jruby-openssl" if defined?(JRUBY_VERSION)
22
+ s.add_development_dependency "rspec", "= 2.13.0"
23
+ s.add_development_dependency "em-spec", "= 0.2.6"
24
+ end
@@ -0,0 +1,285 @@
1
+ require File.expand_path('../spec_helper', __FILE__)
2
+
3
+ describe Pusher::Signature do
4
+ before :each do
5
+ Time.stub!(:now).and_return(Time.at(1234))
6
+
7
+ @token = Pusher::Signature::Token.new('key', 'secret')
8
+
9
+ @request = Pusher::Signature::Request.new('POST', '/some/path', {
10
+ "query" => "params",
11
+ "go" => "here"
12
+ })
13
+ end
14
+
15
+ describe "generating signatures" do
16
+ before :each do
17
+ @signature = "3b237953a5ba6619875cbb2a2d43e8da9ef5824e8a2c689f6284ac85bc1ea0db"
18
+ end
19
+
20
+ it "should generate signature correctly" do
21
+ @request.sign(@token)
22
+ string = @request.send(:string_to_sign)
23
+ string.should == "POST\n/some/path\nauth_key=key&auth_timestamp=1234&auth_version=1.0&go=here&query=params"
24
+
25
+ digest = OpenSSL::Digest::SHA256.new
26
+ signature = OpenSSL::HMAC.hexdigest(digest, @token.secret, string)
27
+ signature.should == @signature
28
+ end
29
+
30
+ it "should make auth_hash available after request is signed" do
31
+ @request.query_hash = {
32
+ "query" => "params"
33
+ }
34
+ lambda {
35
+ @request.auth_hash
36
+ }.should raise_error('Request not signed')
37
+
38
+ @request.sign(@token)
39
+ @request.auth_hash.should == {
40
+ :auth_signature => "da078fcedd72941b6c873caa40d0d6b2000ebfc700cee802b128dd20f72e74e9",
41
+ :auth_version => "1.0",
42
+ :auth_key => "key",
43
+ :auth_timestamp => '1234'
44
+ }
45
+ end
46
+
47
+ it "should cope with symbol keys" do
48
+ @request.query_hash = {
49
+ :query => "params",
50
+ :go => "here"
51
+ }
52
+ @request.sign(@token)[:auth_signature].should == @signature
53
+ end
54
+
55
+ it "should cope with upcase keys (keys are lowercased before signing)" do
56
+ @request.query_hash = {
57
+ "Query" => "params",
58
+ "GO" => "here"
59
+ }
60
+ @request.sign(@token)[:auth_signature].should == @signature
61
+ end
62
+
63
+ it "should generate correct string when query hash contains array" do
64
+ @request.query_hash = {
65
+ "things" => ["thing1", "thing2"]
66
+ }
67
+ @request.send(:string_to_sign).should == "POST\n/some/path\nthings[]=thing1&things[]=thing2"
68
+ end
69
+
70
+ # This may well change in auth version 2
71
+ it "should not escape keys or values in the query string" do
72
+ @request.query_hash = {
73
+ "key;" => "value@"
74
+ }
75
+ @request.send(:string_to_sign).should == "POST\n/some/path\nkey;=value@"
76
+ end
77
+
78
+ it "should cope with requests where the value is nil (antiregression)" do
79
+ @request.query_hash = {
80
+ "key" => nil
81
+ }
82
+ @request.send(:string_to_sign).should == "POST\n/some/path\nkey="
83
+ end
84
+
85
+ it "should use the path to generate signature" do
86
+ @request.path = '/some/other/path'
87
+ @request.sign(@token)[:auth_signature].should_not == @signature
88
+ end
89
+
90
+ it "should use the query string keys to generate signature" do
91
+ @request.query_hash = {
92
+ "other" => "query"
93
+ }
94
+ @request.sign(@token)[:auth_signature].should_not == @signature
95
+ end
96
+
97
+ it "should use the query string values to generate signature" do
98
+ @request.query_hash = {
99
+ "key" => "notfoo",
100
+ "other" => 'bar'
101
+ }
102
+ @request.sign(@token)[:signature].should_not == @signature
103
+ end
104
+ end
105
+
106
+ describe "verification" do
107
+ before :each do
108
+ @request.sign(@token)
109
+ @params = @request.signed_params
110
+ end
111
+
112
+ it "should verify requests" do
113
+ request = Pusher::Signature::Request.new('POST', '/some/path', @params)
114
+ request.authenticate_by_token(@token).should == true
115
+ end
116
+
117
+ it "should raise error if signature is not correct" do
118
+ @params[:auth_signature] = 'asdf'
119
+ request = Pusher::Signature::Request.new('POST', '/some/path', @params)
120
+ lambda {
121
+ request.authenticate_by_token!(@token)
122
+ }.should raise_error('Invalid signature: you should have sent HmacSHA256Hex("POST\n/some/path\nauth_key=key&auth_timestamp=1234&auth_version=1.0&go=here&query=params", your_secret_key), but you sent "asdf"')
123
+ end
124
+
125
+ it "should raise error if timestamp not available" do
126
+ @params.delete(:auth_timestamp)
127
+ request = Pusher::Signature::Request.new('POST', '/some/path', @params)
128
+ lambda {
129
+ request.authenticate_by_token!(@token)
130
+ }.should raise_error('Timestamp required')
131
+ end
132
+
133
+ it "should raise error if timestamp has expired (default of 600s)" do
134
+ request = Pusher::Signature::Request.new('POST', '/some/path', @params)
135
+ Time.stub!(:now).and_return(Time.at(1234 + 599))
136
+ request.authenticate_by_token!(@token).should == true
137
+ Time.stub!(:now).and_return(Time.at(1234 - 599))
138
+ request.authenticate_by_token!(@token).should == true
139
+ Time.stub!(:now).and_return(Time.at(1234 + 600))
140
+ lambda {
141
+ request.authenticate_by_token!(@token)
142
+ }.should raise_error("Timestamp expired: Given timestamp (1970-01-01T00:20:34Z) not within 600s of server time (1970-01-01T00:30:34Z)")
143
+ Time.stub!(:now).and_return(Time.at(1234 - 600))
144
+ lambda {
145
+ request.authenticate_by_token!(@token)
146
+ }.should raise_error("Timestamp expired: Given timestamp (1970-01-01T00:20:34Z) not within 600s of server time (1970-01-01T00:10:34Z)")
147
+ end
148
+
149
+ it "should be possible to customize the timeout grace period" do
150
+ grace = 10
151
+ request = Pusher::Signature::Request.new('POST', '/some/path', @params)
152
+ Time.stub!(:now).and_return(Time.at(1234 + grace - 1))
153
+ request.authenticate_by_token!(@token, grace).should == true
154
+ Time.stub!(:now).and_return(Time.at(1234 + grace))
155
+ lambda {
156
+ request.authenticate_by_token!(@token, grace)
157
+ }.should raise_error("Timestamp expired: Given timestamp (1970-01-01T00:20:34Z) not within 10s of server time (1970-01-01T00:20:44Z)")
158
+ end
159
+
160
+ it "should be possible to skip timestamp check by passing nil" do
161
+ request = Pusher::Signature::Request.new('POST', '/some/path', @params)
162
+ Time.stub!(:now).and_return(Time.at(1234 + 1000))
163
+ request.authenticate_by_token!(@token, nil).should == true
164
+ end
165
+
166
+ it "should check that auth_version is supplied" do
167
+ @params.delete(:auth_version)
168
+ request = Pusher::Signature::Request.new('POST', '/some/path', @params)
169
+ lambda {
170
+ request.authenticate_by_token!(@token)
171
+ }.should raise_error('Version required')
172
+ end
173
+
174
+ it "should check that auth_version equals 1.0" do
175
+ @params[:auth_version] = '1.1'
176
+ request = Pusher::Signature::Request.new('POST', '/some/path', @params)
177
+ lambda {
178
+ request.authenticate_by_token!(@token)
179
+ }.should raise_error('Version not supported')
180
+ end
181
+
182
+ it "should validate that the provided token has a non-empty secret" do
183
+ token = Pusher::Signature::Token.new('key', '')
184
+ request = Pusher::Signature::Request.new('POST', '/some/path', @params)
185
+
186
+ lambda {
187
+ request.authenticate_by_token!(token)
188
+ }.should raise_error('Provided token is missing secret')
189
+ end
190
+
191
+ describe "when used with optional block" do
192
+ it "should optionally take a block which yields the signature" do
193
+ request = Pusher::Signature::Request.new('POST', '/some/path', @params)
194
+ request.authenticate do |key|
195
+ key.should == @token.key
196
+ @token
197
+ end.should == @token
198
+ end
199
+
200
+ it "should raise error if no auth_key supplied to request" do
201
+ @params.delete(:auth_key)
202
+ request = Pusher::Signature::Request.new('POST', '/some/path', @params)
203
+ lambda {
204
+ request.authenticate { |key| nil }
205
+ }.should raise_error('Missing parameter: auth_key')
206
+ end
207
+
208
+ it "should raise error if block returns nil (i.e. key doesn't exist)" do
209
+ request = Pusher::Signature::Request.new('POST', '/some/path', @params)
210
+ lambda {
211
+ request.authenticate { |key| nil }
212
+ }.should raise_error('Unknown auth_key')
213
+ end
214
+
215
+ it "should raise unless block given" do
216
+ request = Pusher::Signature::Request.new('POST', '/some/path', @params)
217
+ lambda {
218
+ request.authenticate
219
+ }.should raise_error(ArgumentError, "Block required")
220
+ end
221
+ end
222
+
223
+ describe "authenticate_async" do
224
+ include EM::SpecHelper
225
+ default_timeout 1
226
+
227
+ it "returns a deferrable which succeeds if authentication passes" do
228
+ request = Pusher::Signature::Request.new('POST', '/some/path', @params)
229
+ em {
230
+ df = EM::DefaultDeferrable.new
231
+
232
+ request_df = request.authenticate_async do |key|
233
+ df
234
+ end
235
+
236
+ df.succeed(@token)
237
+
238
+ request_df.callback { |token|
239
+ token.should == @token
240
+ done
241
+ }
242
+ }
243
+ end
244
+
245
+ it "returns a deferrable which fails if block df fails" do
246
+ request = Pusher::Signature::Request.new('POST', '/some/path', @params)
247
+ em {
248
+ df = EM::DefaultDeferrable.new
249
+
250
+ request_df = request.authenticate_async do |key|
251
+ df
252
+ end
253
+
254
+ df.fail()
255
+
256
+ request_df.errback { |e|
257
+ e.class.should == Pusher::Signature::AuthenticationError
258
+ e.message.should == 'Unknown auth_key'
259
+ done
260
+ }
261
+ }
262
+ end
263
+
264
+ it "returns a deferrable which fails if request does not validate" do
265
+ request = Pusher::Signature::Request.new('POST', '/some/path', @params)
266
+ em {
267
+ df = EM::DefaultDeferrable.new
268
+
269
+ request_df = request.authenticate_async do |key|
270
+ df
271
+ end
272
+
273
+ token = Pusher::Signature::Token.new('key', 'wrong_secret')
274
+ df.succeed(token)
275
+
276
+ request_df.errback { |e|
277
+ e.class.should == Pusher::Signature::AuthenticationError
278
+ e.message.should =~ /Invalid signature/
279
+ done
280
+ }
281
+ }
282
+ end
283
+ end
284
+ end
285
+ end
@@ -0,0 +1,9 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ require 'pusher-signature'
3
+
4
+ require 'rspec'
5
+ require 'em-spec/rspec'
6
+
7
+ RSpec.configure do |config|
8
+
9
+ end
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pusher-signature
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.8
5
+ platform: ruby
6
+ authors:
7
+ - Martyn Loughran
8
+ - Pusher Ltd
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2015-09-29 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - '='
19
+ - !ruby/object:Gem::Version
20
+ version: 2.13.0
21
+ type: :development
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - '='
26
+ - !ruby/object:Gem::Version
27
+ version: 2.13.0
28
+ - !ruby/object:Gem::Dependency
29
+ name: em-spec
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - '='
33
+ - !ruby/object:Gem::Version
34
+ version: 0.2.6
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - '='
40
+ - !ruby/object:Gem::Version
41
+ version: 0.2.6
42
+ description: Simple key/secret based authentication for apis
43
+ email:
44
+ - me@mloughran.com
45
+ - support@pusher.com
46
+ executables: []
47
+ extensions: []
48
+ extra_rdoc_files: []
49
+ files:
50
+ - ".gitignore"
51
+ - ".travis.yml"
52
+ - CHANGELOG.md
53
+ - Gemfile
54
+ - Gemfile.lock
55
+ - LICENSE
56
+ - README.md
57
+ - Rakefile
58
+ - lib/pusher-signature.rb
59
+ - lib/pusher/signature.rb
60
+ - lib/pusher/signature/query_encoder.rb
61
+ - lib/pusher/signature/version.rb
62
+ - pusher-signature.gemspec
63
+ - spec/signature_spec.rb
64
+ - spec/spec_helper.rb
65
+ homepage: http://github.com/pusher/pusher-signature
66
+ licenses:
67
+ - MIT
68
+ metadata: {}
69
+ post_install_message:
70
+ rdoc_options: []
71
+ require_paths:
72
+ - lib
73
+ required_ruby_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ required_rubygems_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ requirements: []
84
+ rubyforge_project:
85
+ rubygems_version: 2.4.8
86
+ signing_key:
87
+ specification_version: 4
88
+ summary: Simple key/secret based authentication for apis
89
+ test_files:
90
+ - spec/signature_spec.rb
91
+ - spec/spec_helper.rb