signed_api 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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 417c51a567e81d9aa1947d5b449390fb8485954b
4
+ data.tar.gz: ac5806bd0571ec2fcab13a968f4c4917ee347dff
5
+ SHA512:
6
+ metadata.gz: 7a8abf9335e3adf9e102e0ca505c9643962318f76abfa4d920f84b182c18ce98c44f241235943c79968af787cbb02b15285f7ee3a5fb574ea6dd7674addf014a
7
+ data.tar.gz: 67a134983ee25e04458927b0ffb6c0c373df4607cca18c90de2fbd8f5ea92b960a1cc1a2223ca3eadb840d1f22996608db03c2bc1dd9d70ffd48887799fca1f6
@@ -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 signed_api.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 ykmr1224
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,48 @@
1
+ # SignedApi
2
+
3
+ SignedApi gem offers easy way to make your web APIs secure by using secret key based signature authentication.
4
+ This uses the similar way as AWS's signed URLs.
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ gem 'signed_api'
11
+
12
+ And then execute:
13
+
14
+ $ bundle
15
+
16
+ Or install it yourself as:
17
+
18
+ $ gem install signed_api
19
+
20
+ ## Usage
21
+
22
+ ### Client side
23
+ You can easily sign your params by sign_params method
24
+ ```ruby
25
+ signed_params = SignedApi::sign_params('GET', '/api/search', {a: 'param_a', b: 'param_b', c: 'param_c'}, 'SOME_KEY', 'SOME_SECRET_STRING', 60)
26
+ ```
27
+ or you can directly make a signed URL like this.
28
+ ```ruby
29
+ signed_url = SignedApi::get_signed_url('https://example.com', 'GET', '/api/search', {a: 'param_a', b: 'param_b', c: 'param_c'}, 'SOME_KEY', 'SOME_SECRET_STRING', 60)
30
+ ```
31
+
32
+ ### Server side
33
+ You can verify the request easily.
34
+ ```ruby
35
+ begin
36
+ SignedApi::verify_signature!(method, path, params) {|key| secrets[key]}
37
+ rescue
38
+ # log error and return error to the client
39
+ end
40
+ ```
41
+
42
+ ## Contributing
43
+
44
+ 1. Fork it
45
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
46
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
47
+ 4. Push to the branch (`git push origin my-new-feature`)
48
+ 5. Create new Pull Request
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,110 @@
1
+ require "signed_api/version"
2
+ require 'openssl'
3
+ require 'uri'
4
+ require 'cgi'
5
+ require 'base64'
6
+
7
+ module SignedApi
8
+ extend self
9
+
10
+ class MissingParameterError < RuntimeError; end
11
+ class AuthSecretNotFoundError < RuntimeError; end
12
+ class SignatureExpiredError < RuntimeError; end
13
+ class SignatureUnmatchError < RuntimeError; end
14
+
15
+ # Returns url signed by the key, secret, and expiry
16
+ #
17
+ # Parameter examples
18
+ # root_url : "http://example.com"
19
+ # method : "GET"/"POST"/etc
20
+ # path : "/some/useful/api"
21
+ # params : {:param1 => "value1", :param2 => "value2"}
22
+ # key : "SomeKeyStringForYourSecretKey"
23
+ # secret : "anysecretstring"
24
+ # expiry_limit : 60 #the signature will be expired in 60 sec
25
+ def get_signed_url(root_url, method, path, params, key, secret, expiry_limit=60)
26
+ params = ApiHelper::sign_params(method, path, params, key, secret, expiry_limit)
27
+ root_url + path + '?' + ApiHelper::normalize_params(params)
28
+ end
29
+
30
+ def get_signed_path(method, path, params, key, secret, expiry_limit=60)
31
+ params = ApiHelper::sign_params(method, path, params, key, secret, expiry_limit)
32
+ path + '?' + ApiHelper::normalize_params(params)
33
+ end
34
+
35
+ # Returns signature added parameter hash
36
+ #
37
+ # method: HTTP METHOD ('GET', 'POST', etc)
38
+ # path: invoked path ('/api/search', '/api/get', etc)
39
+ # params: http params ({param1: value1, param2: value2}, etc)
40
+ # key: authentication key (any string)
41
+ # secret: authentication secret (any string)
42
+ # expiry_limit: the request will expire in expiry_limit seconds (integer)
43
+ def sign_params(method, path, params, key, secret, expiry_limit=60)
44
+ raise ArgumentError, "Expected string for method parameter" unless method.kind_of?(String)
45
+ raise ArgumentError, "Expected string for path parameter" unless path.kind_of?(String)
46
+ raise ArgumentError, "Expected hash for params parameter" unless params.kind_of?(Hash)
47
+ raise ArgumentError, "Expected string for key parameter" unless key.kind_of?(String)
48
+ raise ArgumentError, "Expected string for secret parameter" unless secret.kind_of?(String)
49
+ raise ArgumentError, "Expected integer for expiry_limit parameter" unless expiry_limit.kind_of?(Integer)
50
+ raise ArgumentError, "Expected params not contain auth_key/auth_hash/expiry" unless params[:auth_key].nil? && params[:auth_hash].nil? && params[:expiry].nil?
51
+ res_params = params.merge(auth_key: key, expiry: (Time.now.utc.to_i + expiry_limit).to_s)
52
+ string_to_sign = signed_string(method, path, res_params)
53
+ res_params[:auth_hash] = sha256_hmac_base64(secret, string_to_sign)
54
+ return res_params
55
+ end
56
+
57
+ # Verify input params contains valid signature
58
+ #
59
+ # This method will raise an error if the verification failed.
60
+ def verify_signature!(method, path, params, &get_secret)
61
+ auth_hash = params[:auth_hash]
62
+
63
+ # duplicate params without :auth_hash
64
+ params = params.reject{|key| key==:auth_hash}
65
+ auth_key = params[:auth_key]
66
+ expiry = params[:expiry]
67
+ raise MissingParameterError, "auth_key, auth_hash, or expiry is missing" if auth_key.nil? || auth_hash.nil? || expiry.nil?
68
+
69
+ secret = get_secret.call(auth_key)
70
+ raise AuthSecretNotFoundError, "auth_secret for the auth_key is not found" if secret.nil?
71
+
72
+ now = Time.now.utc.to_i.to_s
73
+ raise SignatureExpiredError if now > expiry
74
+
75
+ raise SignatureUnmatchError, "auth_hash did not match" if auth_hash != gen_authhash(method, path, params, auth_key, secret)
76
+
77
+ return true
78
+ end
79
+
80
+ # Verify input params contain valid signature
81
+ #
82
+ # This method merely returns the result of verification by true or false.
83
+ def verify_signature(method, path, params, &get_secret)
84
+ begin
85
+ return true if verify_signature!(method, path, params, &get_secret)
86
+ rescue MissingParameterError, AuthSecretNotFoundError, SignatureExpiredError, SignatureUnmatchError
87
+ return false
88
+ end
89
+ end
90
+
91
+ def normalize_params(params)
92
+ params.collect{|key, value| "#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}"}.compact.sort! * "&"
93
+ end
94
+
95
+ protected
96
+
97
+ def signed_string(method, path, params)
98
+ "#{method}\n#{path}\n#{normalize_params(params)}"
99
+ end
100
+
101
+ def sha256_hmac_base64(secret, string_to_sign)
102
+ digest = OpenSSL::Digest::SHA256.new
103
+ Base64.strict_encode64(OpenSSL::HMAC.digest(digest, secret, string_to_sign))
104
+ end
105
+
106
+ def gen_authhash(method, path, params, key, secret)
107
+ string_to_sign = signed_string(method, path, params)
108
+ sha256_hmac_base64(secret, string_to_sign)
109
+ end
110
+ end
@@ -0,0 +1,3 @@
1
+ module SignedApi
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'signed_api/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "signed_api"
8
+ spec.version = SignedApi::VERSION
9
+ spec.authors = ["ykmr1224"]
10
+ spec.email = ["ykmr1224@gmail.com"]
11
+ spec.description = %q{SignedApi gem offers easy way to make your web APIs secure by using secret key based signature authentication.}
12
+ spec.summary = %q{SignedApi gem offers easy way to make your web APIs secure by using secret key based signature authentication.}
13
+ spec.homepage = "https://github.com/ykmr1224"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.3"
22
+ spec.add_development_dependency "rake"
23
+ spec.add_development_dependency "rspec"
24
+ end
@@ -0,0 +1,105 @@
1
+ require 'spec_helper'
2
+ require 'signed_api'
3
+
4
+ describe SignedApi do
5
+ it 'should have a version number' do
6
+ expect(SignedApi::VERSION).not_to be_nil
7
+ end
8
+
9
+ describe "#sign_params" do
10
+ it "signs parameter properly" do
11
+ params = SignedApi::sign_params("GET", "/api/search", {a: "param_a", b: "param_b", c: "param_c"}, "key", "secret", 10)
12
+ expect(params[:auth_key]).to eql("key")
13
+ expect(params[:auth_hash]).not_to be_nil
14
+ expect(params[:expiry]).not_to be_nil
15
+ expect(params[:expiry].to_i).to be > Time.now.utc.to_i
16
+ expect(params[:expiry].to_i).to be < Time.now.utc.to_i+20
17
+ expect(params[:a]).to eql("param_a")
18
+ expect(params[:b]).to eql("param_b")
19
+ expect(params[:c]).to eql("param_c")
20
+ end
21
+
22
+ it "handle exceptional case properly" do
23
+ expect {
24
+ SignedApi::sign_params(nil, "/api/search", {a: "param_a"}, "key", "secret", 10)
25
+ }.to raise_error
26
+
27
+ expect {
28
+ SignedApi::sign_params('GET', nil, {a: "param_a"}, "key", "secret", 10)
29
+ }.to raise_error
30
+
31
+ expect {
32
+ SignedApi::sign_params('GET', "/api/search", nil, "key", "secret", 10)
33
+ }.to raise_error
34
+
35
+ expect {
36
+ SignedApi::sign_params('GET', "/api/search", {a: "param_a"}, nil, "secret", 10)
37
+ }.to raise_error
38
+
39
+ expect {
40
+ SignedApi::sign_params('GET', "/api/search", {a: "param_a"}, "key", nil, 10)
41
+ }.to raise_error
42
+
43
+ expect {
44
+ SignedApi::sign_params('GET', "/api/search", {a: "param_a"}, "key", "secret", "10")
45
+ }.to raise_error
46
+
47
+ expect {
48
+ SignedApi::sign_params('GET', "/api/search", {a: "param_a", auth_key: "hoge"}, "key", "secret", 10)
49
+ }.to raise_error
50
+
51
+ expect {
52
+ SignedApi::sign_params('GET', "/api/search", {a: "param_a", auth_hash: "hoge"}, "key", "secret", 10)
53
+ }.to raise_error
54
+
55
+ expect {
56
+ SignedApi::sign_params('GET', "/api/search", {a: "param_a", expiry: "hoge"}, "key", "secret", 10)
57
+ }.to raise_error
58
+ end
59
+ end
60
+
61
+ describe "#verify_signature!" do
62
+ it "verify properly" do
63
+ params = SignedApi::sign_params("POST", "/api/find", {a: "param_a", b: "param_b", c: "param_c"}, "key", "secret", 10)
64
+ expect(SignedApi::verify_signature!("POST", "/api/find", params){|key| "secret"}).to be true
65
+
66
+ key = "123456789ABCDEF"
67
+ secret = "123456789ABCDEF0123456789ABCDEF0"
68
+ params = SignedApi::sign_params("GET", "/", {a: "param_a", b: "param_b", c: "param_c"}, key, secret, 10)
69
+ expect(SignedApi::verify_signature!("GET", "/", params){|key| secret}).to be true
70
+
71
+ params = SignedApi::sign_params("GET", "/", {}, key, secret, 10)
72
+ expect(SignedApi::verify_signature!("GET", "/", params){|key| secret}).to be true
73
+ end
74
+
75
+ it "reject properly" do
76
+ params = SignedApi::sign_params("POST", "/api/find", {a: "param_a", b: "param_b", c: "param_c"}, "key", "secret", 30)
77
+ expect{ SignedApi::verify_signature!("GET", "/api/find", params){|key| "secret"} }.to raise_error(SignedApi::SignatureUnmatchError)
78
+ expect{ SignedApi::verify_signature!("POST", "/api/finds", params){|key| "secret"} }.to raise_error(SignedApi::SignatureUnmatchError)
79
+ expect{ SignedApi::verify_signature!("POST", "api/find", params){|key| "secret"} }.to raise_error(SignedApi::SignatureUnmatchError)
80
+ expect{ SignedApi::verify_signature!("POST", "/api/find", params){|key| "wrongsecret"} }.to raise_error(SignedApi::SignatureUnmatchError)
81
+ expect{ SignedApi::verify_signature!("POST", "/api/find", params){|key| ""} }.to raise_error(SignedApi::SignatureUnmatchError)
82
+ params[:a] = "param_"
83
+ expect{ SignedApi::verify_signature!("POST", "/api/find", params){|key| ""} }.to raise_error(SignedApi::SignatureUnmatchError)
84
+ params[:a] = "param_a"
85
+ params[:x] = "param_x"
86
+ expect{ SignedApi::verify_signature!("POST", "/api/find", params){|key| ""} }.to raise_error(SignedApi::SignatureUnmatchError)
87
+ end
88
+
89
+ it "handle exceptional case properly" do
90
+ params = SignedApi::sign_params("POST", "/api/find", {a: "param_a", b: "param_b", c: "param_c"}, "key", "secret", 10)
91
+ expect{ SignedApi::verify_signature!("POST", "", params){|key| nil} }.to raise_error(SignedApi::AuthSecretNotFoundError)
92
+
93
+ expect{ SignedApi::verify_signature!("POST", "", params.reject{|k| k==:auth_key}){|key| "secret"} }.to raise_error(SignedApi::MissingParameterError)
94
+ expect{ SignedApi::verify_signature!("POST", "", params.reject{|k| k==:auth_hash}){|key| "secret"} }.to raise_error(SignedApi::MissingParameterError)
95
+ expect{ SignedApi::verify_signature!("POST", "", params.reject{|k| k==:expiry}){|key| "secret"} }.to raise_error(SignedApi::MissingParameterError)
96
+ end
97
+
98
+ it "reject expired signature" do
99
+ params = SignedApi::sign_params("POST", "/api/find", {a: "param_a", b: "param_b", c: "param_c"}, "key", "secret", 0)
100
+ sleep 1
101
+ expect{ SignedApi::verify_signature!("POST", "/api/find", params){|key| "secret"} }.to raise_error(SignedApi::SignatureExpiredError)
102
+ end
103
+
104
+ end
105
+ end
@@ -0,0 +1 @@
1
+ require 'rubygems'
metadata ADDED
@@ -0,0 +1,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: signed_api
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - ykmr1224
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-06-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: SignedApi gem offers easy way to make your web APIs secure by using secret
56
+ key based signature authentication.
57
+ email:
58
+ - ykmr1224@gmail.com
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - .gitignore
64
+ - Gemfile
65
+ - LICENSE.txt
66
+ - README.md
67
+ - Rakefile
68
+ - lib/signed_api.rb
69
+ - lib/signed_api/version.rb
70
+ - signed_api.gemspec
71
+ - spec/signed_api_spec.rb
72
+ - spec/spec_helper.rb
73
+ homepage: https://github.com/ykmr1224
74
+ licenses:
75
+ - MIT
76
+ metadata: {}
77
+ post_install_message:
78
+ rdoc_options: []
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ required_rubygems_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - '>='
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ requirements: []
92
+ rubyforge_project:
93
+ rubygems_version: 2.4.5
94
+ signing_key:
95
+ specification_version: 4
96
+ summary: SignedApi gem offers easy way to make your web APIs secure by using secret
97
+ key based signature authentication.
98
+ test_files:
99
+ - spec/signed_api_spec.rb
100
+ - spec/spec_helper.rb
101
+ has_rdoc: