signature 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,21 @@
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
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Martyn Loughran
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.
data/README.md ADDED
@@ -0,0 +1,37 @@
1
+ signature
2
+ ---------
3
+
4
+ Examples
5
+ ========
6
+
7
+ Client example
8
+
9
+ params = {:some => 'parameters'}
10
+ token = Signature::Token.new(key, secret)
11
+ request = Signature::Request.new('POST', '/api/thing, params)
12
+ auth_hash = request.sign(token)
13
+
14
+ HTTParty.post('http://myservice/api/thing', {
15
+ :query => params.merge(auth_hash)
16
+ })
17
+
18
+ Server example (sinatra)
19
+
20
+ error Signature::AuthenticationError do |controller|
21
+ error = controller.env["sinatra.error"]
22
+ halt 401, "401 UNAUTHORIZED: #{error.message}\n"
23
+ end
24
+
25
+ post '/api/thing' do
26
+ request = Authentication::Request.new('POST', env["REQUEST_PATH"], params)
27
+ token = request.authenticate do |key|
28
+ Signature::Token.new(key, lookup_secret(key))
29
+ end
30
+
31
+ # Do whatever you need to do
32
+ end
33
+
34
+ Copyright
35
+ =========
36
+
37
+ Copyright (c) 2010 Martyn Loughran. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,46 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "signature"
8
+ gem.summary = %Q{Simple key/secret based authentication for apis}
9
+ gem.description = %Q{Simple key/secret based authentication for apis}
10
+ gem.email = "me@mloughran.com"
11
+ gem.homepage = "http://github.com/mloughran/signature"
12
+ gem.authors = ["Martyn Loughran"]
13
+ gem.add_dependency "ruby-hmac"
14
+ gem.add_development_dependency "rspec", ">= 1.2.9"
15
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
16
+ end
17
+ Jeweler::GemcutterTasks.new
18
+ rescue LoadError
19
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
20
+ end
21
+
22
+ require 'spec/rake/spectask'
23
+ Spec::Rake::SpecTask.new(:spec) do |spec|
24
+ spec.libs << 'lib' << 'spec'
25
+ spec.spec_files = FileList['spec/**/*_spec.rb']
26
+ end
27
+
28
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
29
+ spec.libs << 'lib' << 'spec'
30
+ spec.pattern = 'spec/**/*_spec.rb'
31
+ spec.rcov = true
32
+ end
33
+
34
+ task :spec => :check_dependencies
35
+
36
+ task :default => :spec
37
+
38
+ require 'rake/rdoctask'
39
+ Rake::RDocTask.new do |rdoc|
40
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
41
+
42
+ rdoc.rdoc_dir = 'rdoc'
43
+ rdoc.title = "signature #{version}"
44
+ rdoc.rdoc_files.include('README*')
45
+ rdoc.rdoc_files.include('lib/**/*.rb')
46
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
data/lib/signature.rb ADDED
@@ -0,0 +1,142 @@
1
+ require 'hmac-sha2'
2
+ require 'base64'
3
+
4
+ module Signature
5
+ class AuthenticationError < RuntimeError; end
6
+
7
+ class Token
8
+ attr_reader :key, :secret
9
+
10
+ def initialize(key, secret)
11
+ @key, @secret = key, secret
12
+ end
13
+
14
+ def sign(request)
15
+ request.sign(self)
16
+ end
17
+ end
18
+
19
+ class Request
20
+ attr_accessor :path, :query_hash
21
+
22
+ # http://www.w3.org/TR/NOTE-datetime
23
+ ISO8601 = "%Y-%m-%dT%H:%M:%SZ"
24
+
25
+ def initialize(method, path, query)
26
+ raise ArgumentError, "Expected string" unless path.kind_of?(String)
27
+ raise ArgumentError, "Expected hash" unless query.kind_of?(Hash)
28
+
29
+ query_hash = {}
30
+ auth_hash = {}
31
+ query.each do |key, v|
32
+ k = key.to_s.downcase
33
+ k[0..4] == 'auth_' ? auth_hash[k] = v : query_hash[k] = v
34
+ end
35
+
36
+ @method = method.upcase
37
+ @path, @query_hash, @auth_hash = path, query_hash, auth_hash
38
+ end
39
+
40
+ def sign(token)
41
+ @auth_hash = {
42
+ :auth_version => "1.0",
43
+ :auth_key => token.key,
44
+ :auth_timestamp => Time.now.to_i
45
+ }
46
+
47
+ @auth_hash[:auth_signature] = signature(token)
48
+
49
+ return @auth_hash
50
+ end
51
+
52
+ # Authenticates the request with a token
53
+ #
54
+ # Timestamp check: Unless timestamp_grace is set to nil (which will skip
55
+ # the timestamp check), an exception will be raised if timestamp is not
56
+ # supplied or if the timestamp provided is not within timestamp_grace of
57
+ # the real time (defaults to 10 minutes)
58
+ #
59
+ # Signature check: Raises an exception if the signature does not match the
60
+ # computed value
61
+ #
62
+ def authenticate_by_token!(token, timestamp_grace = 600)
63
+ validate_version!
64
+ validate_timestamp!(timestamp_grace)
65
+ validate_signature!(token)
66
+ true
67
+ end
68
+
69
+ def authenticate_by_token(token, timestamp_grace = 600)
70
+ authenticate_by_token!(token, timestamp_grace)
71
+ rescue AuthenticationError
72
+ false
73
+ end
74
+
75
+ def authenticate(timestamp_grace = 600, &block)
76
+ key = @auth_hash['auth_key']
77
+ raise AuthenticationError, "Authentication key required" unless key
78
+ token = yield key
79
+ unless token && token.secret
80
+ raise AuthenticationError, "Invalid authentication key"
81
+ end
82
+ authenticate_by_token!(token, timestamp_grace)
83
+ return token
84
+ end
85
+
86
+ def auth_hash
87
+ raise "Request not signed" unless @auth_hash && @auth_hash[:auth_signature]
88
+ @auth_hash
89
+ end
90
+
91
+ private
92
+
93
+ def signature(token)
94
+ HMAC::SHA256.hexdigest(token.secret, string_to_sign)
95
+ end
96
+
97
+ def string_to_sign
98
+ [@method, @path, parameter_string].join("\n")
99
+ end
100
+
101
+ def parameter_string
102
+ param_hash = @query_hash.merge(@auth_hash || {})
103
+
104
+ # Convert keys to lowercase strings
105
+ hash = {}; param_hash.each { |k,v| hash[k.to_s.downcase] = v }
106
+
107
+ # Exclude signature from signature generation!
108
+ hash.delete("auth_signature")
109
+
110
+ hash.keys.sort.map { |k| "#{k}=#{hash[k]}" }.join("&")
111
+ end
112
+
113
+ def validate_version!
114
+ version = @auth_hash["auth_version"]
115
+ raise AuthenticationError, "Version required" unless version
116
+ raise AuthenticationError, "Version not supported" unless version == '1.0'
117
+ end
118
+
119
+ def validate_timestamp!(grace)
120
+ return true if grace.nil?
121
+
122
+ timestamp = @auth_hash["auth_timestamp"]
123
+ error = (timestamp.to_i - Time.now.to_i).abs
124
+ raise AuthenticationError, "Timestamp required" unless timestamp
125
+ if error >= grace
126
+ raise AuthenticationError, "Timestamp expired: Given timestamp "\
127
+ "(#{Time.at(timestamp.to_i).utc.strftime(ISO8601)}) "\
128
+ "not within #{grace}s of server time "\
129
+ "(#{Time.now.utc.strftime(ISO8601)})"
130
+ end
131
+ return true
132
+ end
133
+
134
+ def validate_signature!(token)
135
+ unless @auth_hash["auth_signature"] == signature(token)
136
+ raise AuthenticationError, "Invalid signature: you should have "\
137
+ "sent HmacSHA256Hex(#{string_to_sign.inspect}, your_secret_key)"
138
+ end
139
+ return true
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,176 @@
1
+ require File.expand_path('../spec_helper', __FILE__)
2
+
3
+ describe Signature do
4
+ before :each do
5
+ Time.stub!(:now).and_return(Time.at(1234))
6
+
7
+ @token = Signature::Token.new('key', 'secret')
8
+
9
+ @request = Signature::Request.new('POST', '/some/path', {
10
+ "query" => "params",
11
+ "go" => "here"
12
+ })
13
+ @signature = @request.sign(@token)[:auth_signature]
14
+ end
15
+
16
+ it "should generate base64 encoded signature from correct key" do
17
+ @request.send(:string_to_sign).should == "POST\n/some/path\nauth_key=key&auth_timestamp=1234&auth_version=1.0&go=here&query=params"
18
+ @signature.should == '3b237953a5ba6619875cbb2a2d43e8da9ef5824e8a2c689f6284ac85bc1ea0db'
19
+ end
20
+
21
+ it "should make auth_hash available after request is signed" do
22
+ request = Signature::Request.new('POST', '/some/path', {
23
+ "query" => "params"
24
+ })
25
+ lambda {
26
+ request.auth_hash
27
+ }.should raise_error('Request not signed')
28
+
29
+ request.sign(@token)
30
+ request.auth_hash.should == {
31
+ :auth_signature => "da078fcedd72941b6c873caa40d0d6b2000ebfc700cee802b128dd20f72e74e9",
32
+ :auth_version => "1.0",
33
+ :auth_key => "key",
34
+ :auth_timestamp => 1234
35
+ }
36
+ end
37
+
38
+ it "should cope with symbol keys" do
39
+ @request.query_hash = {
40
+ :query => "params",
41
+ :go => "here"
42
+ }
43
+ @request.sign(@token)[:auth_signature].should == @signature
44
+ end
45
+
46
+ it "should cope with upcase keys (keys are lowercased before signing)" do
47
+ @request.query_hash = {
48
+ "Query" => "params",
49
+ "GO" => "here"
50
+ }
51
+ @request.sign(@token)[:auth_signature].should == @signature
52
+ end
53
+
54
+ it "should use the path to generate signature" do
55
+ @request.path = '/some/other/path'
56
+ @request.sign(@token)[:auth_signature].should_not == @signature
57
+ end
58
+
59
+ it "should use the query string keys to generate signature" do
60
+ @request.query_hash = {
61
+ "other" => "query"
62
+ }
63
+ @request.sign(@token)[:auth_signature].should_not == @signature
64
+ end
65
+
66
+ it "should use the query string values to generate signature" do
67
+ @request.query_hash = {
68
+ "key" => "notfoo",
69
+ "other" => 'bar'
70
+ }
71
+ @request.sign(@token)[:signature].should_not == @signature
72
+ end
73
+
74
+ describe "verification" do
75
+ before :each do
76
+ Time.stub!(:now).and_return(Time.at(1234))
77
+ @request.sign(@token)
78
+ @params = @request.query_hash.merge(@request.auth_hash)
79
+ end
80
+
81
+ it "should verify requests" do
82
+ request = Signature::Request.new('POST', '/some/path', @params)
83
+ request.authenticate_by_token(@token).should == true
84
+ end
85
+
86
+ it "should raise error if signature is not correct" do
87
+ @params[:auth_signature] = 'asdf'
88
+ request = Signature::Request.new('POST', '/some/path', @params)
89
+ lambda {
90
+ request.authenticate_by_token!(@token)
91
+ }.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)')
92
+ end
93
+
94
+ it "should raise error if timestamp not available" do
95
+ @params.delete(:auth_timestamp)
96
+ request = Signature::Request.new('POST', '/some/path', @params)
97
+ lambda {
98
+ request.authenticate_by_token!(@token)
99
+ }.should raise_error('Timestamp required')
100
+ end
101
+
102
+ it "should raise error if timestamp has expired (default of 600s)" do
103
+ request = Signature::Request.new('POST', '/some/path', @params)
104
+ Time.stub!(:now).and_return(Time.at(1234 + 599))
105
+ request.authenticate_by_token!(@token).should == true
106
+ Time.stub!(:now).and_return(Time.at(1234 - 599))
107
+ request.authenticate_by_token!(@token).should == true
108
+ Time.stub!(:now).and_return(Time.at(1234 + 600))
109
+ lambda {
110
+ request.authenticate_by_token!(@token)
111
+ }.should raise_error("Timestamp expired: Given timestamp (1970-01-01T00:20:34Z) not within 600s of server time (1970-01-01T00:30:34Z)")
112
+ Time.stub!(:now).and_return(Time.at(1234 - 600))
113
+ lambda {
114
+ request.authenticate_by_token!(@token)
115
+ }.should raise_error("Timestamp expired: Given timestamp (1970-01-01T00:20:34Z) not within 600s of server time (1970-01-01T00:10:34Z)")
116
+ end
117
+
118
+ it "should be possible to customize the timeout grace period" do
119
+ grace = 10
120
+ request = Signature::Request.new('POST', '/some/path', @params)
121
+ Time.stub!(:now).and_return(Time.at(1234 + grace - 1))
122
+ request.authenticate_by_token!(@token, grace).should == true
123
+ Time.stub!(:now).and_return(Time.at(1234 + grace))
124
+ lambda {
125
+ request.authenticate_by_token!(@token, grace)
126
+ }.should raise_error("Timestamp expired: Given timestamp (1970-01-01T00:20:34Z) not within 10s of server time (1970-01-01T00:20:44Z)")
127
+ end
128
+
129
+ it "should be possible to skip timestamp check by passing nil" do
130
+ request = Signature::Request.new('POST', '/some/path', @params)
131
+ Time.stub!(:now).and_return(Time.at(1234 + 1000))
132
+ request.authenticate_by_token!(@token, nil).should == true
133
+ end
134
+
135
+ it "should check that auth_version is supplied" do
136
+ @params.delete(:auth_version)
137
+ request = Signature::Request.new('POST', '/some/path', @params)
138
+ lambda {
139
+ request.authenticate_by_token!(@token)
140
+ }.should raise_error('Version required')
141
+ end
142
+
143
+ it "should check that auth_version equals 1.0" do
144
+ @params[:auth_version] = '1.1'
145
+ request = Signature::Request.new('POST', '/some/path', @params)
146
+ lambda {
147
+ request.authenticate_by_token!(@token)
148
+ }.should raise_error('Version not supported')
149
+ end
150
+
151
+ describe "when used with optional block" do
152
+ it "should optionally take a block which yields the signature" do
153
+ request = Signature::Request.new('POST', '/some/path', @params)
154
+ request.authenticate do |key|
155
+ key.should == @token.key
156
+ @token
157
+ end.should == @token
158
+ end
159
+
160
+ it "should raise error if no auth_key supplied to request" do
161
+ @params.delete(:auth_key)
162
+ request = Signature::Request.new('POST', '/some/path', @params)
163
+ lambda {
164
+ request.authenticate { |key| nil }
165
+ }.should raise_error('Authentication key required')
166
+ end
167
+
168
+ it "should raise error if block returns nil (i.e. key doesn't exist)" do
169
+ request = Signature::Request.new('POST', '/some/path', @params)
170
+ lambda {
171
+ request.authenticate { |key| nil }
172
+ }.should raise_error('Invalid authentication key')
173
+ end
174
+ end
175
+ end
176
+ end
data/spec/spec.opts ADDED
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1,11 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+
4
+ require 'rubygems'
5
+ require 'signature'
6
+ require 'spec'
7
+ require 'spec/autorun'
8
+
9
+ Spec::Runner.configure do |config|
10
+
11
+ end
metadata ADDED
@@ -0,0 +1,98 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: signature
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 0
9
+ version: 0.1.0
10
+ platform: ruby
11
+ authors:
12
+ - Martyn Loughran
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-05-07 00:00:00 +01:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: ruby-hmac
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 0
29
+ version: "0"
30
+ type: :runtime
31
+ version_requirements: *id001
32
+ - !ruby/object:Gem::Dependency
33
+ name: rspec
34
+ prerelease: false
35
+ requirement: &id002 !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ segments:
40
+ - 1
41
+ - 2
42
+ - 9
43
+ version: 1.2.9
44
+ type: :development
45
+ version_requirements: *id002
46
+ description: Simple key/secret based authentication for apis
47
+ email: me@mloughran.com
48
+ executables: []
49
+
50
+ extensions: []
51
+
52
+ extra_rdoc_files:
53
+ - LICENSE
54
+ - README.md
55
+ files:
56
+ - .document
57
+ - .gitignore
58
+ - LICENSE
59
+ - README.md
60
+ - Rakefile
61
+ - VERSION
62
+ - lib/signature.rb
63
+ - spec/signature_spec.rb
64
+ - spec/spec.opts
65
+ - spec/spec_helper.rb
66
+ has_rdoc: true
67
+ homepage: http://github.com/mloughran/signature
68
+ licenses: []
69
+
70
+ post_install_message:
71
+ rdoc_options:
72
+ - --charset=UTF-8
73
+ require_paths:
74
+ - lib
75
+ required_ruby_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ segments:
80
+ - 0
81
+ version: "0"
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ segments:
87
+ - 0
88
+ version: "0"
89
+ requirements: []
90
+
91
+ rubyforge_project:
92
+ rubygems_version: 1.3.6
93
+ signing_key:
94
+ specification_version: 3
95
+ summary: Simple key/secret based authentication for apis
96
+ test_files:
97
+ - spec/signature_spec.rb
98
+ - spec/spec_helper.rb