hawk-auth 0.0.0 → 0.1.0
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.
- data/.travis.yml +7 -0
- data/README.md +91 -69
- data/Rakefile +8 -1
- data/hawk-auth.gemspec +2 -0
- data/lib/hawk.rb +5 -2
- data/lib/hawk/authentication_failure.rb +18 -0
- data/lib/hawk/authorization_header.rb +114 -0
- data/lib/hawk/client.rb +22 -0
- data/lib/hawk/crypto.rb +96 -0
- data/lib/hawk/server.rb +54 -0
- data/lib/hawk/version.rb +1 -1
- data/spec/authentication_header_spec.rb +33 -0
- data/spec/client_spec.rb +174 -0
- data/spec/crypto_spec.rb +197 -0
- data/spec/server_spec.rb +290 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/support/shared_examples/authorization_header.rb +154 -0
- metadata +53 -3
data/lib/hawk/server.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
module Hawk
|
2
|
+
module Server
|
3
|
+
extend self
|
4
|
+
|
5
|
+
def authenticate(authorization_header, options)
|
6
|
+
Hawk::AuthorizationHeader.authenticate(authorization_header, options)
|
7
|
+
end
|
8
|
+
|
9
|
+
def authenticate_bewit(bewit, options)
|
10
|
+
padding = '=' * ((4 - bewit.size) % 4)
|
11
|
+
id, timestamp, mac, ext = Base64.decode64(bewit + padding).split('\\')
|
12
|
+
|
13
|
+
unless options[:credentials_lookup].respond_to?(:call) && (credentials = options[:credentials_lookup].call(id))
|
14
|
+
return AuthenticationFailure.new(:id, "Unidentified id")
|
15
|
+
end
|
16
|
+
|
17
|
+
if Time.at(timestamp.to_i) < Time.now
|
18
|
+
return AuthenticationFailure.new(:ts, "Stale timestamp")
|
19
|
+
end
|
20
|
+
|
21
|
+
expected_bewit = Crypto.bewit(
|
22
|
+
:credentials => credentials,
|
23
|
+
:host => options[:host],
|
24
|
+
:path => remove_bewit_param_from_path(options[:path]),
|
25
|
+
:port => options[:port],
|
26
|
+
:method => options[:method],
|
27
|
+
:ts => timestamp,
|
28
|
+
:ext => ext
|
29
|
+
)
|
30
|
+
|
31
|
+
unless expected_bewit == bewit
|
32
|
+
return AuthenticationFailure.new(:bewit, "Invalid signature")
|
33
|
+
end
|
34
|
+
|
35
|
+
credentials
|
36
|
+
end
|
37
|
+
|
38
|
+
def build_authorization_header(options)
|
39
|
+
Hawk::AuthorizationHeader.build(options, [:hash, :ext, :mac])
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def remove_bewit_param_from_path(path)
|
45
|
+
path, query = path.split('?')
|
46
|
+
return path unless query
|
47
|
+
query, fragment = query.split('#')
|
48
|
+
query = query.split('&').reject { |i| i =~ /\Abewit=/ }.join('&')
|
49
|
+
path << "?#{query}" if query != ''
|
50
|
+
path << "#{fragment}" if fragment
|
51
|
+
path
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
data/lib/hawk/version.rb
CHANGED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Hawk::AuthenticationFailure do
|
4
|
+
let(:algorithm) { "sha256" }
|
5
|
+
let(:credentials) do
|
6
|
+
{
|
7
|
+
:id => '123456',
|
8
|
+
:key => '2983d45yun89q',
|
9
|
+
:algorithm => algorithm
|
10
|
+
}
|
11
|
+
end
|
12
|
+
|
13
|
+
describe "#header" do
|
14
|
+
let(:instance) {
|
15
|
+
described_class.new(:mac, "Invalid mac", :credentials => credentials)
|
16
|
+
}
|
17
|
+
|
18
|
+
let(:timestamp) { Time.now.to_i }
|
19
|
+
|
20
|
+
let(:timestamp_mac) {
|
21
|
+
Hawk::Crypto.ts_mac({ :ts => timestamp, :credentials => credentials })
|
22
|
+
}
|
23
|
+
|
24
|
+
before do
|
25
|
+
now = Time.now
|
26
|
+
Time.stubs(:now).returns(now)
|
27
|
+
end
|
28
|
+
|
29
|
+
it "returns valid hawk authentication failure header" do
|
30
|
+
expect(instance.header).to eql(%(Hawk ts="#{timestamp}", tsm="#{timestamp_mac}", error="#{instance.message}"))
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/spec/client_spec.rb
ADDED
@@ -0,0 +1,174 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'support/shared_examples/authorization_header'
|
3
|
+
|
4
|
+
describe Hawk::Client do
|
5
|
+
|
6
|
+
let(:credentials) do
|
7
|
+
{
|
8
|
+
:id => '123456',
|
9
|
+
:key => '2983d45yun89q',
|
10
|
+
:algorithm => algorithm
|
11
|
+
}
|
12
|
+
end
|
13
|
+
|
14
|
+
let(:timestamp) { Time.now.to_i }
|
15
|
+
let(:nonce) { 'Ygvqdz' }
|
16
|
+
|
17
|
+
describe ".authenticate" do
|
18
|
+
let(:payload) {}
|
19
|
+
let(:ext) {}
|
20
|
+
|
21
|
+
let(:input) do
|
22
|
+
_input = {
|
23
|
+
:method => 'POST',
|
24
|
+
:path => '/somewhere/over/the/rainbow',
|
25
|
+
:host => 'example.net',
|
26
|
+
:port => 80,
|
27
|
+
:content_type => 'text/plain',
|
28
|
+
:credentials => credentials,
|
29
|
+
:ts => timestamp,
|
30
|
+
:nonce => nonce
|
31
|
+
}
|
32
|
+
_input[:payload] = payload if payload
|
33
|
+
_input
|
34
|
+
end
|
35
|
+
|
36
|
+
let(:client_input) do
|
37
|
+
_input = input
|
38
|
+
_input[:ext] = ext if ext
|
39
|
+
_input
|
40
|
+
end
|
41
|
+
|
42
|
+
let(:expected_mac) { Hawk::Crypto.mac(client_input) }
|
43
|
+
let(:expected_hash) { client_input[:payload] ? Hawk::Crypto.hash(client_input) : nil }
|
44
|
+
|
45
|
+
let(:authorization_header) do
|
46
|
+
parts = []
|
47
|
+
parts << %(hash="#{expected_hash}") if expected_hash
|
48
|
+
parts << %(mac="#{expected_mac}")
|
49
|
+
parts << %(ext="#{ext}") if ext
|
50
|
+
"Hawk #{parts.join(', ')}"
|
51
|
+
end
|
52
|
+
|
53
|
+
shared_examples "an authorization response header authenticator" do
|
54
|
+
it_behaves_like "an authorization header authenticator"
|
55
|
+
end
|
56
|
+
|
57
|
+
context "when using sha256" do
|
58
|
+
let(:algorithm) { "sha256" }
|
59
|
+
|
60
|
+
it_behaves_like "an authorization response header authenticator"
|
61
|
+
end
|
62
|
+
|
63
|
+
context "when using sha1" do
|
64
|
+
let(:algorithm) { "sha1" }
|
65
|
+
|
66
|
+
it_behaves_like "an authorization response header authenticator"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe ".build_authorization_header" do
|
71
|
+
before do
|
72
|
+
now = Time.now
|
73
|
+
Time.stubs(:now).returns(now)
|
74
|
+
end
|
75
|
+
|
76
|
+
let(:expected_mac) { Hawk::Crypto.mac(input) }
|
77
|
+
let(:expected_hash) { input[:payload] ? Hawk::Crypto.hash(input) : nil }
|
78
|
+
|
79
|
+
let(:input) do
|
80
|
+
_input = {
|
81
|
+
:credentials => credentials,
|
82
|
+
:ts => timestamp,
|
83
|
+
:method => 'POST',
|
84
|
+
:path => '/somewhere/over/the/rainbow',
|
85
|
+
:host => 'example.net',
|
86
|
+
:port => 80,
|
87
|
+
:payload => 'something to write about',
|
88
|
+
:ext => 'Bazinga!'
|
89
|
+
}
|
90
|
+
_input[:nonce] = nonce if nonce
|
91
|
+
_input
|
92
|
+
end
|
93
|
+
|
94
|
+
let(:expected_output_parts) do
|
95
|
+
parts = []
|
96
|
+
parts << %(id="#{credentials[:id]}")
|
97
|
+
parts << %(ts="#{timestamp}")
|
98
|
+
parts << %(nonce="#{input[:nonce]}")
|
99
|
+
parts << %(hash="#{expected_hash}") if input[:payload]
|
100
|
+
parts << %(ext="#{input[:ext]}") if input[:ext]
|
101
|
+
parts << %(mac="#{expected_mac}")
|
102
|
+
parts
|
103
|
+
end
|
104
|
+
|
105
|
+
let(:expected_output) do
|
106
|
+
"Hawk #{expected_output_parts.join(', ')}"
|
107
|
+
end
|
108
|
+
|
109
|
+
shared_examples "an authorization request header builder" do
|
110
|
+
it_behaves_like "an authorization header builder"
|
111
|
+
|
112
|
+
context "without nonce" do
|
113
|
+
let(:nonce) { nil }
|
114
|
+
|
115
|
+
it "generates a nonce" do
|
116
|
+
actual = described_class.build_authorization_header(input)
|
117
|
+
expect(actual).to match(%r{\bnonce="[^"]+"})
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
context "when using sha256" do
|
123
|
+
let(:algorithm) { "sha256" }
|
124
|
+
|
125
|
+
it_behaves_like "an authorization request header builder"
|
126
|
+
end
|
127
|
+
|
128
|
+
context "when using sha1" do
|
129
|
+
let(:algorithm) { "sha1" }
|
130
|
+
|
131
|
+
it_behaves_like "an authorization request header builder"
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
describe ".calculate_time_offset" do
|
136
|
+
let(:algorithm) { 'sha256' }
|
137
|
+
let(:timestamp) { 1365741469 }
|
138
|
+
let(:header) do
|
139
|
+
%(Hawk ts="#{timestamp}", tsm="#{timestamp_mac}", error="Some Error Message")
|
140
|
+
end
|
141
|
+
|
142
|
+
before do
|
143
|
+
Time.stubs(:now).returns(Time.at(timestamp + offset))
|
144
|
+
end
|
145
|
+
|
146
|
+
context "with valid timestamp mac" do
|
147
|
+
let(:timestamp_mac) { Hawk::Crypto.ts_mac(:ts => timestamp, :credentials => credentials) }
|
148
|
+
|
149
|
+
context "with positive offset" do
|
150
|
+
let(:offset) { -2030 }
|
151
|
+
it "returns time offset in seconds" do
|
152
|
+
expect(described_class.calculate_time_offset(header, :credentials => credentials)).to eql(offset * -1)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
context "with negative offset" do
|
157
|
+
let(:offset) { 12345 }
|
158
|
+
it "returns time offset in seconds" do
|
159
|
+
expect(described_class.calculate_time_offset(header, :credentials => credentials)).to eql(offset * -1)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
context "with invalid timestamp mac" do
|
165
|
+
let(:timestamp_mac) { "fooabr" }
|
166
|
+
let(:offset) { 1432 }
|
167
|
+
|
168
|
+
it "returns nil" do
|
169
|
+
expect(described_class.calculate_time_offset(header, :credentials => credentials)).to be_nil
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
end
|
data/spec/crypto_spec.rb
ADDED
@@ -0,0 +1,197 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Hawk::Crypto do
|
4
|
+
|
5
|
+
let(:algorithm) { "sha256" }
|
6
|
+
let(:credentials) do
|
7
|
+
{
|
8
|
+
:id => '123456',
|
9
|
+
:key => '2983d45yun89q',
|
10
|
+
:algorithm => algorithm
|
11
|
+
}
|
12
|
+
end
|
13
|
+
|
14
|
+
describe "#hash" do
|
15
|
+
let(:hashing_method) { "hash" }
|
16
|
+
|
17
|
+
shared_examples "a payload hashing method" do
|
18
|
+
it "returns valid base64 encoded hash of payload" do
|
19
|
+
expect(described_class.send(hashing_method, input)).to eql(expected_output)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
let(:input) do
|
24
|
+
{
|
25
|
+
:credentials => credentials,
|
26
|
+
:ts => 1353809207,
|
27
|
+
:nonce => 'Ygvqdz',
|
28
|
+
:method => 'POST',
|
29
|
+
:path => '/somewhere/over/the/rainbow',
|
30
|
+
:host => 'example.net',
|
31
|
+
:port => 80,
|
32
|
+
:payload => 'something to write about',
|
33
|
+
:ext => 'Bazinga!'
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
context "when using sha1 algorithm" do
|
38
|
+
let(:expected_output) { "bsvY3IfUllw6V5rvk4tStEvpBhE=" }
|
39
|
+
let(:algorithm) { "sha1" }
|
40
|
+
|
41
|
+
it_behaves_like "a payload hashing method"
|
42
|
+
end
|
43
|
+
|
44
|
+
context "when using sha256 algorithm" do
|
45
|
+
let(:expected_output) { "LjRmtkSKTW0ObTUyZ7N+vjClKd//KTTdfhF1M4XCuEM=" }
|
46
|
+
|
47
|
+
it_behaves_like "a payload hashing method"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
shared_examples "a mac digest method" do
|
52
|
+
it "returns valid base64 encoded hmac" do
|
53
|
+
expect(described_class.send(mac_digest_method, input)).to eql(expected_output)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe ".bewit" do
|
58
|
+
let(:mac_digest_method) { "bewit" }
|
59
|
+
|
60
|
+
context "when using sha256 algorithm" do
|
61
|
+
let(:input) do
|
62
|
+
{
|
63
|
+
:credentials => credentials,
|
64
|
+
:method => 'GET',
|
65
|
+
:path => '/resource/4?a=1&b=2',
|
66
|
+
:host => 'example.com',
|
67
|
+
:port => 80,
|
68
|
+
:ext => 'some-app-data',
|
69
|
+
:ttl => 60 * 60 * 24 * 365 * 100
|
70
|
+
}
|
71
|
+
end
|
72
|
+
|
73
|
+
let(:expected_output) {
|
74
|
+
"MTIzNDU2XDQ1MTkzMTE0NThcYkkwanFlS1prUHE0V1hRMmkxK0NrQ2lOanZEc3BSVkNGajlmbElqMXphWT1cc29tZS1hcHAtZGF0YQ"
|
75
|
+
}
|
76
|
+
|
77
|
+
before do
|
78
|
+
Time.stubs(:now).returns(Time.at(1365711458))
|
79
|
+
end
|
80
|
+
|
81
|
+
it_behaves_like "a mac digest method"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
describe ".mac" do
|
86
|
+
let(:mac_digest_method) { "mac" }
|
87
|
+
|
88
|
+
let(:input) do
|
89
|
+
{
|
90
|
+
:credentials => credentials,
|
91
|
+
:ts => 1353809207,
|
92
|
+
:nonce => 'Ygvqdz',
|
93
|
+
:method => 'POST',
|
94
|
+
:path => '/somewhere/over/the/rainbow',
|
95
|
+
:host => 'example.net',
|
96
|
+
:port => 80,
|
97
|
+
:payload => 'something to write about',
|
98
|
+
:ext => 'Bazinga!'
|
99
|
+
}
|
100
|
+
end
|
101
|
+
|
102
|
+
context "when using sha1 algorithm" do
|
103
|
+
let(:expected_output) { "qbf1ZPG/r/e06F4ht+T77LXi5vw=" }
|
104
|
+
let(:algorithm) { "sha1" }
|
105
|
+
|
106
|
+
it_behaves_like "a mac digest method"
|
107
|
+
end
|
108
|
+
|
109
|
+
context "when using sha256 algorithm" do
|
110
|
+
let(:expected_output) { "dh5kEkotNusOuHPolRYUhvy2vlhJybTC2pqBdUQk5z0=" }
|
111
|
+
|
112
|
+
it_behaves_like "a mac digest method"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
describe ".ts_mac" do
|
117
|
+
let(:input) do
|
118
|
+
{
|
119
|
+
:credentials => credentials,
|
120
|
+
:ts => 1365741469
|
121
|
+
}
|
122
|
+
end
|
123
|
+
|
124
|
+
it "returns valid timestamp mac" do
|
125
|
+
expect(described_class.ts_mac(input)).to eql("h/Ff6XI1euObD78ZNflapvLKXGuaw1RiLI4Q6Q5sAbM=")
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
describe ".normalized_string" do
|
130
|
+
let(:normalization_method) { "normalized_string" }
|
131
|
+
|
132
|
+
shared_examples "an input normalization method" do
|
133
|
+
it "returns a valid normalized string" do
|
134
|
+
expect(described_class.send(normalization_method, input)).to eql(expected_output)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
let(:input) do
|
139
|
+
{
|
140
|
+
:ts => 1365701514,
|
141
|
+
:nonce => '5b4e',
|
142
|
+
:method => 'GET',
|
143
|
+
:path => "/path/to/foo?bar=baz",
|
144
|
+
:host => 'example.com',
|
145
|
+
:port => 8080
|
146
|
+
}
|
147
|
+
end
|
148
|
+
|
149
|
+
let(:expected_output) do
|
150
|
+
%(hawk.1.header\n#{input[:ts]}\n#{input[:nonce]}\n#{input[:method]}\n#{input[:path]}\n#{input[:host]}\n#{input[:port]}\n\n\n)
|
151
|
+
end
|
152
|
+
|
153
|
+
it_behaves_like "an input normalization method"
|
154
|
+
|
155
|
+
context "with ext" do
|
156
|
+
let(:input) do
|
157
|
+
{
|
158
|
+
:ts => 1365701514,
|
159
|
+
:nonce => '5b4e',
|
160
|
+
:method => 'GET',
|
161
|
+
:path => '/path/to/foo?bar=baz',
|
162
|
+
:host => 'example.com',
|
163
|
+
:port => 8080,
|
164
|
+
:ext => 'this is some app data'
|
165
|
+
}
|
166
|
+
end
|
167
|
+
|
168
|
+
let(:expected_output) do
|
169
|
+
%(hawk.1.header\n#{input[:ts]}\n#{input[:nonce]}\n#{input[:method]}\n#{input[:path]}\n#{input[:host]}\n#{input[:port]}\n\n#{input[:ext]}\n)
|
170
|
+
end
|
171
|
+
|
172
|
+
it_behaves_like "an input normalization method"
|
173
|
+
end
|
174
|
+
|
175
|
+
context "with payload and ext" do
|
176
|
+
let(:input) do
|
177
|
+
{
|
178
|
+
:ts => 1365701514,
|
179
|
+
:nonce => '5b4e',
|
180
|
+
:method => 'GET',
|
181
|
+
:path => '/path/to/foo?bar=baz',
|
182
|
+
:host => 'example.com',
|
183
|
+
:port => 8080,
|
184
|
+
:hash => 'U4MKKSmiVxk37JCCrAVIjV/OhB3y+NdwoCr6RShbVkE=',
|
185
|
+
:ext => 'this is some app data'
|
186
|
+
}
|
187
|
+
end
|
188
|
+
|
189
|
+
let(:expected_output) do
|
190
|
+
%(hawk.1.header\n#{input[:ts]}\n#{input[:nonce]}\n#{input[:method]}\n#{input[:path]}\n#{input[:host]}\n#{input[:port]}\n#{input[:hash]}\n#{input[:ext]}\n)
|
191
|
+
end
|
192
|
+
|
193
|
+
it_behaves_like "an input normalization method"
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
end
|