authmac 1.0.4 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +51 -1
- data/example/app.rb +16 -6
- data/example/public/app.css +13 -0
- data/example/views/form.erb +2 -1
- data/example/views/sign.erb +18 -11
- data/lib/authmac/hmac_checker.rb +32 -3
- data/lib/authmac/version.rb +1 -1
- data/spec/authmac/hmac_checker_spec.rb +38 -2
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 829ec585cf6a375f48ac8173e6d0ac78b74cbc80
|
4
|
+
data.tar.gz: 1dac8a149c91af398587addbf5b1438d48ca13db
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dfd1e0b844b368b2c943861f6c1e7c89c11a96149083477ee48dd735cd54a4da6406d414b2dcb801da6519e75d7edb3df567dfca183af97ddaaf5c1ddebc2f11
|
7
|
+
data.tar.gz: 6e2d287aa49a7aad085e83fa85b0d206f877568148c0216f0b66b1b0b6fc6a7875471e249106eac47aee74aee3248a90c18c7da7b173de7ec4b4b40f850d2ead
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,9 @@
|
|
1
|
+
## Version 2.0.0 / 2016-11-08
|
2
|
+
|
3
|
+
* BREAKING: keyword arguments for HmacChecker.new
|
4
|
+
* Allow nested hashes/arrays to be signed.
|
5
|
+
* Add :json message_format to sign a sorted json string, instead of just values.
|
6
|
+
|
1
7
|
## Version 1.0.4 / 2014-06-20
|
2
8
|
|
3
9
|
* Added HmacChecker#with_signature which returns the given hash with the HMAC merged in.
|
data/README.md
CHANGED
@@ -21,7 +21,57 @@ Or install it yourself as:
|
|
21
21
|
|
22
22
|
## Usage
|
23
23
|
|
24
|
-
|
24
|
+
```
|
25
|
+
checker = Authmac::HmacChecker.new(secret, parameter_separator: '|', digest_function: 'sha1', message_format: :json)
|
26
|
+
checker.with_signature(params_to_send)
|
27
|
+
```
|
28
|
+
|
29
|
+
* message_format: :values or :json. Format of message-string to sign. default :values.
|
30
|
+
* digest_function: string to give `OpenSSL::Digest.new(digest_function)`
|
31
|
+
* parameter_separator: Used for :values format to separate values.
|
32
|
+
|
33
|
+
## Examples
|
34
|
+
|
35
|
+
|
36
|
+
```
|
37
|
+
params_to_sign = {
|
38
|
+
'some_param' => 'foo',
|
39
|
+
'other_param' => 'bar',
|
40
|
+
'timestamp' => timestamp.to_s, # 12345678
|
41
|
+
'nonce' => SecureRandom.urlsafe_base64(4), # ILFtHg
|
42
|
+
'consumer_key' => 'my_key'
|
43
|
+
}
|
44
|
+
```
|
45
|
+
|
46
|
+
Classic syntax: signs values, sorted by key, joined by '|'
|
47
|
+
|
48
|
+
```
|
49
|
+
checker = Authmac::HmacChecker.new(ENV['consumer_secret'],
|
50
|
+
parameter_separator: '|',
|
51
|
+
digest_function: 'sha256')
|
52
|
+
params_to_send = checker.with_signature(params_to_sign)
|
53
|
+
|
54
|
+
checker.send(:message_string, params_to_sign) # "my_key|ILFtHg|bar|foo|123456789"
|
55
|
+
```
|
56
|
+
|
57
|
+
Syntax for more complex parameters, signs json, sorted by key (subhashes as well).
|
58
|
+
|
59
|
+
```
|
60
|
+
params_to_sign['some_param'] = {foo: [1, {bar: 2}]}
|
61
|
+
```
|
62
|
+
|
63
|
+
```
|
64
|
+
checker = Authmac::HmacChecker.new(ENV['consumer_secret'],
|
65
|
+
digest_function: 'sha256',
|
66
|
+
message_format: :json)
|
67
|
+
params_to_send = checker.with_signature(params_to_sign)
|
68
|
+
|
69
|
+
checker.send(:message_string, params_to_sign)
|
70
|
+
# '{"consumer_key":"my_key","nonce":"ILFtHg","other_params":"bar",' \
|
71
|
+
# '"some_param":{"foo":["1",{"bar":"2"}]},"timestamp":"12345678"}'
|
72
|
+
|
73
|
+
```
|
74
|
+
|
25
75
|
|
26
76
|
## Contributing
|
27
77
|
|
data/example/app.rb
CHANGED
@@ -1,25 +1,35 @@
|
|
1
1
|
require 'sinatra'
|
2
2
|
require 'cgi'
|
3
3
|
require 'authmac'
|
4
|
+
require 'json'
|
4
5
|
|
5
6
|
set :app_file, __FILE__
|
7
|
+
def hmac_secret
|
8
|
+
"very_secret_string_of_at_least_the_length_of_the_hash_so_64_for_sha256"
|
9
|
+
end
|
6
10
|
|
7
11
|
get '/' do
|
8
12
|
erb :form
|
9
13
|
end
|
10
14
|
|
11
15
|
post '/sign' do
|
12
|
-
@
|
13
|
-
@
|
14
|
-
@
|
15
|
-
@
|
16
|
+
@params = params.select { |_k, v| v != '' }
|
17
|
+
@secret = hmac_secret
|
18
|
+
@checker = Authmac::HmacChecker.new(hmac_secret, '|', 'sha256')
|
19
|
+
@params_to_sign = @params.merge \
|
20
|
+
'timestamp' => Time.now.to_i.to_s,
|
21
|
+
'version' => '3',
|
22
|
+
'nonce' => 'implementing_apps_should_store_this_to_prevent_replays',
|
23
|
+
'consumer_key' => 'key_to_find_secret'
|
24
|
+
@hmac = @checker.sign(@params_to_sign)
|
25
|
+
@params_with_hmac = @params_to_sign.merge('hmac' => @hmac)
|
16
26
|
@link = @params_with_hmac.map{|k,v| "#{k}=#{CGI.escape(v.to_s)}" }.join("&")
|
17
27
|
|
18
28
|
erb :sign
|
19
29
|
end
|
20
30
|
|
21
31
|
get '/auth' do
|
22
|
-
hmac_checker = Authmac::HmacChecker.new(
|
32
|
+
hmac_checker = Authmac::HmacChecker.new(hmac_secret, '|', 'sha256')
|
23
33
|
timestamp_checker = Authmac::TimestampChecker.new(30, 10)
|
24
34
|
authenticator = Authmac::Authenticator.new(hmac_checker, timestamp_checker)
|
25
35
|
@validation = authenticator.validate(params)
|
@@ -31,4 +41,4 @@ get '/auth' do
|
|
31
41
|
elsif @validation.timestamp_failure?
|
32
42
|
erb :auth_timestamp_failure
|
33
43
|
end
|
34
|
-
end
|
44
|
+
end
|
data/example/views/form.erb
CHANGED
@@ -4,6 +4,7 @@
|
|
4
4
|
<form method="post" action="/sign">
|
5
5
|
Professional ID: <input type="text" name="userid" /> <br/>
|
6
6
|
Patient ID: <input type="text" name="clientid" /> <br/>
|
7
|
-
|
7
|
+
Professional First Name: <input type="text" name="user_firstname" /> <br />
|
8
|
+
Professional Last Name: <input type="text" name="user_lastname" /> <br />
|
8
9
|
<input type="submit" value="Generate URL" />
|
9
10
|
</form>
|
data/example/views/sign.erb
CHANGED
@@ -1,22 +1,29 @@
|
|
1
1
|
<!DOCTYPE html>
|
2
|
+
<link rel="stylesheet" type="text/css" href="/app.css">
|
3
|
+
|
2
4
|
<title>HMAC Generator</title>
|
3
5
|
<h1>Okay, here's what I've got</h1>
|
4
6
|
|
5
|
-
<p>These are the values we used to generate the hash
|
6
|
-
<code><%= @params
|
7
|
+
<p>These are the values we used to generate the hash:</p>
|
8
|
+
<code><%= JSON.pretty_generate @params %></code>
|
9
|
+
<p class="info">Removed blank params here, we won't send them to the provider, if we had, they should have been in the string_to_sign e.g. "1|2||4".</p>
|
7
10
|
|
8
|
-
<p>Then we add the timestamp parameter
|
9
|
-
<code
|
11
|
+
<p>Then we add the timestamp and other parameter we want:</p>
|
12
|
+
<code>params = <%= JSON.pretty_generate @params_to_sign %></code>
|
13
|
+
<p class="info">timestamp is the only key that is required. <br />
|
14
|
+
version, nonce and consumer_key are app-specific and can be anything you want, just make sure you calculate the hmac over all the params you send.
|
15
|
+
</p>
|
10
16
|
|
11
|
-
<p>To calculate the HMAC, we
|
12
|
-
<code
|
17
|
+
<p>To calculate the HMAC, we sort the params by key alfabetically and join the values with a '|' in between:</p>
|
18
|
+
<code>string_to_sign = <%= @checker.send(:message_string, @params_to_sign) %></code>
|
13
19
|
|
14
|
-
<p>And use
|
15
|
-
<code>
|
20
|
+
<p>And use the configured secret for the HMAC generation:</p>
|
21
|
+
<code>hmac_secret = '<%= @secret %>'</code>
|
16
22
|
|
17
|
-
<p>Then we add the HMAC
|
18
|
-
<code
|
23
|
+
<p>Then we add the HMAC to the params:</p>
|
24
|
+
<code># Authmac::HmacChecker.new(hmac_secret, '|', 'sha256').sign(params)
|
25
|
+
<%= JSON.pretty_generate @params_with_hmac %></code>
|
19
26
|
|
20
|
-
<p>Then, this is a nice URL to use. We URL-escape each value, and keep in mind that the order of HTTP-
|
27
|
+
<p>Then, this is a nice URL to use. We URL-escape each value, and keep in mind that the order of HTTP-methods does not matter:</p>
|
21
28
|
|
22
29
|
<a href="/auth?<%= @link %>">Link</a>
|
data/lib/authmac/hmac_checker.rb
CHANGED
@@ -2,10 +2,16 @@ require 'openssl'
|
|
2
2
|
|
3
3
|
module Authmac
|
4
4
|
class HmacChecker
|
5
|
-
|
5
|
+
# @param message_format [symbol] `:values` or `:json`.
|
6
|
+
# `:json` will use a sorted json string to sign.
|
7
|
+
# `:values` will use the sorted values separated by `parameter_separator` to sign.
|
8
|
+
def initialize(secret, message_format: :values,
|
9
|
+
parameter_separator: '|',
|
10
|
+
digest_function: 'sha1')
|
6
11
|
@secret = secret
|
7
12
|
@digest = digest_function
|
8
13
|
@separator = parameter_separator
|
14
|
+
@message_format = message_format
|
9
15
|
fail Authmac::SecretError, 'secret too short, see rfc2104' unless @secret.bytes.size >= digester.digest_length * 2
|
10
16
|
end
|
11
17
|
|
@@ -28,12 +34,35 @@ module Authmac
|
|
28
34
|
end
|
29
35
|
|
30
36
|
def message_string(hash)
|
31
|
-
|
37
|
+
fail ArgumentError, 'hash arg not a hash' unless hash.is_a? Hash
|
38
|
+
|
39
|
+
case @message_format
|
40
|
+
when :values
|
41
|
+
hash_values_sorted_by_key(hash).flatten.join(@separator)
|
42
|
+
when :json
|
43
|
+
require 'json'
|
44
|
+
JSON.generate(params_sorted_by_key(hash))
|
45
|
+
else
|
46
|
+
fail ArgumentError, 'unknown message_format'
|
47
|
+
end
|
32
48
|
end
|
33
49
|
|
34
50
|
def hash_values_sorted_by_key(hash)
|
35
51
|
hash.sort_by {|key, value| key }.map(&:last)
|
36
52
|
end
|
53
|
+
|
54
|
+
# stringifies and sorts hashes by key at all levels.
|
55
|
+
def params_sorted_by_key(params)
|
56
|
+
case params
|
57
|
+
when Hash
|
58
|
+
params.map { |k, v| [k.to_s, params_sorted_by_key(v)] }
|
59
|
+
.sort_by { |k, v| k }
|
60
|
+
.to_h
|
61
|
+
when Array
|
62
|
+
params.map { |val| params_sorted_by_key(val) }
|
63
|
+
else
|
64
|
+
params
|
65
|
+
end
|
66
|
+
end
|
37
67
|
end
|
38
68
|
end
|
39
|
-
|
data/lib/authmac/version.rb
CHANGED
@@ -2,11 +2,11 @@ require 'authmac/hmac_checker'
|
|
2
2
|
|
3
3
|
module Authmac
|
4
4
|
describe HmacChecker do
|
5
|
-
let(:checker) { HmacChecker.new('very secret random key of sufficient size'
|
5
|
+
let(:checker) { HmacChecker.new('very secret random key of sufficient size') }
|
6
6
|
|
7
7
|
it 'raises an error for a secret shorter than the hmac output' do
|
8
8
|
expect {
|
9
|
-
HmacChecker.new('way too short key'
|
9
|
+
HmacChecker.new('way too short key')
|
10
10
|
}.to raise_error SecretError, 'secret too short, see rfc2104'
|
11
11
|
end
|
12
12
|
|
@@ -45,6 +45,42 @@ module Authmac
|
|
45
45
|
hmacify('parameter|another'))).to be_truthy
|
46
46
|
end
|
47
47
|
end
|
48
|
+
|
49
|
+
context 'with json message_string' do
|
50
|
+
let(:checker) { HmacChecker.new('very secret random key of sufficient size', message_format: :json) }
|
51
|
+
|
52
|
+
it 'creates a sorted json string' do
|
53
|
+
hash = {first: '1', third: '6', second: {b: ['3', {c: '4', d: '5'}], a: '2'}}
|
54
|
+
expect(checker.send :message_string, hash)
|
55
|
+
.to eq '{"first":"1","second":{"a":"2","b":["3",{"c":"4","d":"5"}]},"third":"6"}'
|
56
|
+
end
|
57
|
+
|
58
|
+
context 'for a hash with nested hashes' do
|
59
|
+
it 'succeeds with correct hmac' do
|
60
|
+
expect(checker.validate({first: '1', second: {a: '2', b: '3'}, third: '4'},
|
61
|
+
hmacify('{"first":"1","second":{"a":"2","b":"3"},"third":"4"}'))).to be_truthy
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'sorts hash values based on their keys' do
|
65
|
+
expect(checker.validate({first: '1', third: '4', second: {b: '3', a: '2'}},
|
66
|
+
hmacify('{"first":"1","second":{"a":"2","b":"3"},"third":"4"}'))).to be_truthy
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
context 'for a hash with nested hashes and arrays' do
|
71
|
+
it 'succeeds with correct hmac' do
|
72
|
+
expect(checker.validate({first: '1', second: {a: '2', b: ['3', '4']}, third: '5'},
|
73
|
+
hmacify('{"first":"1","second":{"a":"2","b":["3","4"]},"third":"5"}')))
|
74
|
+
.to be_truthy
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'sorts hash values based on their keys' do
|
78
|
+
expect(checker.validate({first: '1', third: '6', second: {b: ['3', {c: '4', d: '5'}], a: '2'}},
|
79
|
+
hmacify( '{"first":"1","second":{"a":"2","b":["3",{"c":"4","d":"5"}]},"third":"6"}')))
|
80
|
+
.to be_truthy
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
48
84
|
end
|
49
85
|
|
50
86
|
describe '#with_signature' do
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: authmac
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Marten Veldthuis
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2016-11-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -56,6 +56,7 @@ files:
|
|
56
56
|
- authmac.gemspec
|
57
57
|
- circle.yml
|
58
58
|
- example/app.rb
|
59
|
+
- example/public/app.css
|
59
60
|
- example/views/auth_hmac_failure.erb
|
60
61
|
- example/views/auth_success.erb
|
61
62
|
- example/views/auth_timestamp_failure.erb
|
@@ -87,7 +88,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
87
88
|
version: '0'
|
88
89
|
requirements: []
|
89
90
|
rubyforge_project:
|
90
|
-
rubygems_version: 2.
|
91
|
+
rubygems_version: 2.4.8
|
91
92
|
signing_key:
|
92
93
|
specification_version: 4
|
93
94
|
summary: Single Sign-On implementation based on HMAC.
|
@@ -95,4 +96,3 @@ test_files:
|
|
95
96
|
- spec/authmac/hmac_checker_spec.rb
|
96
97
|
- spec/authmac/timestamp_checker_spec.rb
|
97
98
|
- spec/authmac_spec.rb
|
98
|
-
has_rdoc:
|