the_signature 0.0.1

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.
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in the_signature.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Ramario Depass
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,29 @@
1
+ # TheSignature
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'the_signature'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install the_signature
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,224 @@
1
+ require 'openssl'
2
+
3
+ require 'the_signature/query_encoder'
4
+
5
+ module TheSignature
6
+ class AuthenticationError < RuntimeError; end
7
+
8
+ class Token
9
+ attr_reader :key, :secret
10
+
11
+ def initialize(key, secret)
12
+ @key, @secret = key, secret
13
+ end
14
+
15
+ def sign(request)
16
+ request.sign(self)
17
+ end
18
+ end
19
+
20
+ class Request
21
+ attr_accessor :path, :query_hash
22
+
23
+ include QueryEncoder
24
+
25
+ # http://www.w3.org/TR/NOTE-datetime
26
+ ISO8601 = "%Y-%m-%dT%H:%M:%SZ"
27
+
28
+ def initialize(method, path, query)
29
+ raise ArgumentError, "Expected string" unless path.kind_of?(String)
30
+ raise ArgumentError, "Expected hash" unless query.kind_of?(Hash)
31
+
32
+ query_hash = {}
33
+ auth_hash = {}
34
+ query.each do |key, v|
35
+ k = key.to_s.downcase
36
+ k[0..4] == 'auth_' ? auth_hash[k] = v : query_hash[k] = v
37
+ end
38
+
39
+ @method = method.upcase
40
+ @path, @query_hash, @auth_hash = path, query_hash, auth_hash
41
+ @signed = false
42
+ end
43
+
44
+ # Sign the request with the given token, and return the computed
45
+ # authentication parameters
46
+ #
47
+ def sign(token)
48
+ @auth_hash = {
49
+ :auth_version => "1.0",
50
+ :auth_key => token.key,
51
+ :auth_timestamp => Time.now.to_i.to_s
52
+ }
53
+ @auth_hash[:auth_signature] = signature(token)
54
+
55
+ @signed = true
56
+
57
+ return @auth_hash
58
+ end
59
+
60
+ # Authenticates the request with a token
61
+ #
62
+ # Raises an AuthenticationError if the request is invalid.
63
+ # AuthenticationError exception messages are designed to be exposed to API
64
+ # consumers, and should help them correct errors generating signatures
65
+ #
66
+ # Timestamp: Unless timestamp_grace is set to nil (which allows this check
67
+ # to be skipped), AuthenticationError will be raised if the timestamp is
68
+ # missing or further than timestamp_grace period away from the real time
69
+ # (defaults to 10 minutes)
70
+ #
71
+ # Signature: Raises AuthenticationError if the signature does not match
72
+ # the computed HMAC. The error contains a hint for how to sign.
73
+ #
74
+ def authenticate_by_token!(token, timestamp_grace = 600)
75
+ # Validate that your code has provided a valid token. This does not
76
+ # raise an AuthenticationError since passing tokens with empty secret is
77
+ # a code error which should be fixed, not reported to the API's consumer
78
+ if token.secret.nil? || token.secret.empty?
79
+ raise "Provided token is missing secret"
80
+ end
81
+
82
+ validate_version!
83
+ validate_timestamp!(timestamp_grace)
84
+ validate_signature!(token)
85
+ true
86
+ end
87
+
88
+ # Authenticate the request with a token, but rather than raising an
89
+ # exception if the request is invalid, simply returns false
90
+ #
91
+ def authenticate_by_token(token, timestamp_grace = 600)
92
+ authenticate_by_token!(token, timestamp_grace)
93
+ rescue AuthenticationError
94
+ false
95
+ end
96
+
97
+ # Authenticate a request
98
+ #
99
+ # Takes a block which will be called with the auth_key from the request,
100
+ # and which should return a Signature::Token (or nil if no token can be
101
+ # found for the key)
102
+ #
103
+ # Raises errors in the same way as authenticate_by_token!
104
+ #
105
+ def authenticate(timestamp_grace = 600)
106
+ raise ArgumentError, "Block required" unless block_given?
107
+ key = @auth_hash['auth_key']
108
+ raise AuthenticationError, "Missing parameter: auth_key" unless key
109
+ token = yield key
110
+ unless token
111
+ raise AuthenticationError, "Unknown auth_key"
112
+ end
113
+ authenticate_by_token!(token, timestamp_grace)
114
+ return token
115
+ end
116
+
117
+ # Authenticate a request asynchronously
118
+ #
119
+ # This method is useful it you're running a server inside eventmachine and
120
+ # need to lookup the token asynchronously.
121
+ #
122
+ # The block is passed an auth key and a deferrable which should succeed
123
+ # with the token, or fail if the token cannot be found
124
+ #
125
+ # This method returns a deferrable which succeeds with the valid token, or
126
+ # fails with an AuthenticationError which can be used to pass the error
127
+ # back to the user
128
+ #
129
+ def authenticate_async(timestamp_grace = 600)
130
+ raise ArgumentError, "Block required" unless block_given?
131
+ df = EM::DefaultDeferrable.new
132
+
133
+ key = @auth_hash['auth_key']
134
+
135
+ unless key
136
+ df.fail(AuthenticationError.new("Missing parameter: auth_key"))
137
+ return
138
+ end
139
+
140
+ token_df = yield key
141
+ token_df.callback { |token|
142
+ begin
143
+ authenticate_by_token!(token, timestamp_grace)
144
+ df.succeed(token)
145
+ rescue AuthenticationError => e
146
+ df.fail(e)
147
+ end
148
+ }
149
+ token_df.errback {
150
+ df.fail(AuthenticationError.new("Unknown auth_key"))
151
+ }
152
+ ensure
153
+ return df
154
+ end
155
+
156
+ # Expose the authentication parameters for a signed request
157
+ #
158
+ def auth_hash
159
+ raise "Request not signed" unless @signed
160
+ @auth_hash
161
+ end
162
+
163
+ # Query parameters merged with the computed authentication parameters
164
+ #
165
+ def signed_params
166
+ @query_hash.merge(auth_hash)
167
+ end
168
+
169
+ private
170
+
171
+ def signature(token)
172
+ digest = OpenSSL::Digest::SHA256.new
173
+ OpenSSL::HMAC.hexdigest(digest, token.secret, string_to_sign)
174
+ end
175
+
176
+ def string_to_sign
177
+ [@method, @path, parameter_string].join("\n")
178
+ end
179
+
180
+ def parameter_string
181
+ param_hash = @query_hash.merge(@auth_hash || {})
182
+
183
+ # Convert keys to lowercase strings
184
+ hash = {}; param_hash.each { |k,v| hash[k.to_s.downcase] = v }
185
+
186
+ # Exclude signature from signature generation!
187
+ hash.delete("auth_signature")
188
+
189
+ hash.sort.map do |k, v|
190
+ QueryEncoder.encode_param_without_escaping(k, v)
191
+ end.join('&')
192
+ end
193
+
194
+ def validate_version!
195
+ version = @auth_hash["auth_version"]
196
+ raise AuthenticationError, "Version required" unless version
197
+ raise AuthenticationError, "Version not supported" unless version == '1.0'
198
+ end
199
+
200
+ def validate_timestamp!(grace)
201
+ return true if grace.nil?
202
+
203
+ timestamp = @auth_hash["auth_timestamp"]
204
+ error = (timestamp.to_i - Time.now.to_i).abs
205
+ raise AuthenticationError, "Timestamp required" unless timestamp
206
+ if error >= grace
207
+ raise AuthenticationError, "Timestamp expired: Given timestamp "\
208
+ "(#{Time.at(timestamp.to_i).utc.strftime(ISO8601)}) "\
209
+ "not within #{grace}s of server time "\
210
+ "(#{Time.now.utc.strftime(ISO8601)})"
211
+ end
212
+ return true
213
+ end
214
+
215
+ def validate_signature!(token)
216
+ unless @auth_hash["auth_signature"] == signature(token)
217
+ raise AuthenticationError, "Invalid signature: you should have "\
218
+ "sent HmacSHA256Hex(#{string_to_sign.inspect}, your_secret_key)"\
219
+ ", but you sent #{@auth_hash["auth_signature"].inspect}"
220
+ end
221
+ return true
222
+ end
223
+ end
224
+ end
@@ -0,0 +1,3 @@
1
+ module TheSignature
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,20 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'the_signature/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "the_signature"
8
+ gem.version = TheSignature::VERSION
9
+ gem.platform = Gem::Platform::RUBY
10
+ gem.authors = ["Ramario Depass"]
11
+ gem.email = ["RamarioDepass@gmail.com"]
12
+ gem.description = %q{Simple key/secret based authentication for apis}
13
+ gem.summary = %q{Simple key/secret based authentication for apis}
14
+ gem.homepage = "http://github.com/ramariodepass/the_signature"
15
+
16
+ gem.files = `git ls-files`.split($/)
17
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
18
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
19
+ gem.require_paths = ["lib"]
20
+ end
metadata ADDED
@@ -0,0 +1,53 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: the_signature
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Ramario Depass
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-08-24 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: Simple key/secret based authentication for apis
15
+ email:
16
+ - RamarioDepass@gmail.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - .gitignore
22
+ - Gemfile
23
+ - LICENSE.txt
24
+ - README.md
25
+ - Rakefile
26
+ - lib/the_signature.rb
27
+ - lib/the_signature/version.rb
28
+ - the_signature.gemspec
29
+ homepage: http://github.com/ramariodepass/the_signature
30
+ licenses: []
31
+ post_install_message:
32
+ rdoc_options: []
33
+ require_paths:
34
+ - lib
35
+ required_ruby_version: !ruby/object:Gem::Requirement
36
+ none: false
37
+ requirements:
38
+ - - ! '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ required_rubygems_version: !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ! '>='
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ requirements: []
48
+ rubyforge_project:
49
+ rubygems_version: 1.8.25
50
+ signing_key:
51
+ specification_version: 3
52
+ summary: Simple key/secret based authentication for apis
53
+ test_files: []