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/spec/server_spec.rb
ADDED
@@ -0,0 +1,290 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'support/shared_examples/authorization_header'
|
3
|
+
|
4
|
+
describe Hawk::Server do
|
5
|
+
let(:credentials) do
|
6
|
+
{
|
7
|
+
:id => '123456',
|
8
|
+
:key => '2983d45yun89q',
|
9
|
+
:algorithm => algorithm
|
10
|
+
}
|
11
|
+
end
|
12
|
+
|
13
|
+
describe ".authenticate" do
|
14
|
+
let(:credentials_lookup) do
|
15
|
+
lambda { |id|
|
16
|
+
if id == credentials[:id]
|
17
|
+
credentials
|
18
|
+
end
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
let(:nonce_lookup) do
|
23
|
+
lambda { |nonce| nil }
|
24
|
+
end
|
25
|
+
|
26
|
+
let(:payload) {}
|
27
|
+
let(:ext) {}
|
28
|
+
let(:timestamp) { Time.now.to_i }
|
29
|
+
let(:nonce) { 'Ygvqdz' }
|
30
|
+
|
31
|
+
let(:input) do
|
32
|
+
_input = {
|
33
|
+
:method => 'POST',
|
34
|
+
:path => '/somewhere/over/the/rainbow',
|
35
|
+
:host => 'example.net',
|
36
|
+
:port => 80,
|
37
|
+
:content_type => 'text/plain',
|
38
|
+
:credentials_lookup => credentials_lookup,
|
39
|
+
:nonce_lookup => nonce_lookup
|
40
|
+
}
|
41
|
+
_input[:payload] = payload if payload
|
42
|
+
_input
|
43
|
+
end
|
44
|
+
|
45
|
+
let(:client_input) do
|
46
|
+
_input = input.merge(
|
47
|
+
:credentials => credentials,
|
48
|
+
:ts => timestamp,
|
49
|
+
:nonce => nonce
|
50
|
+
)
|
51
|
+
_input[:ext] = ext if ext
|
52
|
+
_input
|
53
|
+
end
|
54
|
+
|
55
|
+
let(:expected_mac) { Hawk::Crypto.mac(client_input) }
|
56
|
+
let(:expected_hash) { client_input[:payload] ? Hawk::Crypto.hash(client_input) : nil }
|
57
|
+
|
58
|
+
let(:authorization_header) do
|
59
|
+
parts = []
|
60
|
+
parts << %(id="#{credentials[:id]}")
|
61
|
+
parts << %(ts="#{timestamp}")
|
62
|
+
parts << %(nonce="#{nonce}") if nonce
|
63
|
+
parts << %(hash="#{expected_hash}") if expected_hash
|
64
|
+
parts << %(mac="#{expected_mac}")
|
65
|
+
parts << %(ext="#{ext}") if ext
|
66
|
+
"Hawk #{parts.join(', ')}"
|
67
|
+
end
|
68
|
+
|
69
|
+
shared_examples "an authorization request header authenticator" do
|
70
|
+
it_behaves_like "an authorization header authenticator"
|
71
|
+
|
72
|
+
context "when unidentified id" do
|
73
|
+
let(:credentials_lookup) do
|
74
|
+
lambda { |id| }
|
75
|
+
end
|
76
|
+
|
77
|
+
it "returns error object" do
|
78
|
+
actual = described_class.authenticate(authorization_header, input)
|
79
|
+
expect(actual).to be_a(Hawk::AuthenticationFailure)
|
80
|
+
expect(actual.key).to eql(:id)
|
81
|
+
expect(actual.message).to_not eql(nil)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
context "when stale timestamp" do
|
86
|
+
context "when too old" do
|
87
|
+
let(:timestamp) { Time.now.to_i - 1001 }
|
88
|
+
|
89
|
+
it "returns error object" do
|
90
|
+
actual = described_class.authenticate(authorization_header, input)
|
91
|
+
expect(actual).to be_a(Hawk::AuthenticationFailure)
|
92
|
+
expect(actual.key).to eql(:ts)
|
93
|
+
expect(actual.message).to_not eql(nil)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
context "when too far in the future" do
|
98
|
+
let(:timestamp) { Time.now.to_i + 1001 }
|
99
|
+
|
100
|
+
it "returns error object" do
|
101
|
+
actual = described_class.authenticate(authorization_header, input)
|
102
|
+
expect(actual).to be_a(Hawk::AuthenticationFailure)
|
103
|
+
expect(actual.key).to eql(:ts)
|
104
|
+
expect(actual.message).to_not eql(nil)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
context "when invalid content type" do
|
109
|
+
let(:payload) { 'baz' }
|
110
|
+
before do
|
111
|
+
client_input[:content_type] = 'application/foo'
|
112
|
+
end
|
113
|
+
|
114
|
+
it "returns error object" do
|
115
|
+
actual = described_class.authenticate(authorization_header, input)
|
116
|
+
expect(actual).to be_a(Hawk::AuthenticationFailure)
|
117
|
+
expect(actual.key).to eql(:mac)
|
118
|
+
expect(actual.message).to_not eql(nil)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
context "when nonce missing" do
|
123
|
+
let(:nonce) { nil }
|
124
|
+
|
125
|
+
it "returns error object" do
|
126
|
+
actual = described_class.authenticate(authorization_header, input)
|
127
|
+
expect(actual).to be_a(Hawk::AuthenticationFailure)
|
128
|
+
expect(actual.key).to eql(:nonce)
|
129
|
+
expect(actual.message).to_not eql(nil)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
context "when replay" do
|
135
|
+
let(:nonce_lookup) do
|
136
|
+
lambda do |nonce|
|
137
|
+
true
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
it "returns error object" do
|
142
|
+
actual = described_class.authenticate(authorization_header, input)
|
143
|
+
expect(actual).to be_a(Hawk::AuthenticationFailure)
|
144
|
+
expect(actual.key).to eql(:nonce)
|
145
|
+
expect(actual.message).to_not eql(nil)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
context "when no credentials_lookup given" do
|
150
|
+
before do
|
151
|
+
input.delete(:credentials_lookup)
|
152
|
+
end
|
153
|
+
|
154
|
+
it "returns error object" do
|
155
|
+
actual = described_class.authenticate(authorization_header, input)
|
156
|
+
expect(actual).to be_a(Hawk::AuthenticationFailure)
|
157
|
+
expect(actual.key).to eql(:id)
|
158
|
+
expect(actual.message).to_not eql(nil)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
context "when using sha256" do
|
164
|
+
let(:algorithm) { "sha256" }
|
165
|
+
|
166
|
+
it_behaves_like "an authorization request header authenticator"
|
167
|
+
end
|
168
|
+
|
169
|
+
context "when using sha1" do
|
170
|
+
let(:algorithm) { "sha1" }
|
171
|
+
|
172
|
+
it_behaves_like "an authorization request header authenticator"
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
describe ".build_authorization_header" do
|
177
|
+
let(:expected_mac) { Hawk::Crypto.mac(input) }
|
178
|
+
let(:expected_hash) { input[:payload] ? Hawk::Crypto.hash(input) : nil }
|
179
|
+
let(:timestamp) { Time.now.to_i }
|
180
|
+
let(:nonce) { 'Ygvqdz' }
|
181
|
+
|
182
|
+
let(:input) do
|
183
|
+
_input = {
|
184
|
+
:credentials => credentials,
|
185
|
+
:ts => timestamp,
|
186
|
+
:method => 'POST',
|
187
|
+
:path => '/somewhere/over/the/rainbow',
|
188
|
+
:host => 'example.net',
|
189
|
+
:port => 80,
|
190
|
+
:payload => 'something to write about',
|
191
|
+
:ext => 'Bazinga!'
|
192
|
+
}
|
193
|
+
_input[:nonce] = nonce if nonce
|
194
|
+
_input
|
195
|
+
end
|
196
|
+
|
197
|
+
let(:expected_output_parts) do
|
198
|
+
parts = []
|
199
|
+
parts << %(hash="#{expected_hash}") if input[:payload]
|
200
|
+
parts << %(ext="#{input[:ext]}") if input[:ext]
|
201
|
+
parts << %(mac="#{expected_mac}")
|
202
|
+
parts
|
203
|
+
end
|
204
|
+
|
205
|
+
let(:expected_output) do
|
206
|
+
"Hawk #{expected_output_parts.join(', ')}"
|
207
|
+
end
|
208
|
+
|
209
|
+
context "when using sha256" do
|
210
|
+
let(:algorithm) { "sha256" }
|
211
|
+
|
212
|
+
it_behaves_like "an authorization header builder"
|
213
|
+
end
|
214
|
+
|
215
|
+
context "when using sha1" do
|
216
|
+
let(:algorithm) { "sha1" }
|
217
|
+
|
218
|
+
it_behaves_like "an authorization header builder"
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
describe ".authenticate_bewit" do
|
223
|
+
let(:credentials_lookup) do
|
224
|
+
lambda { |id|
|
225
|
+
if id == credentials[:id]
|
226
|
+
credentials
|
227
|
+
end
|
228
|
+
}
|
229
|
+
end
|
230
|
+
|
231
|
+
let(:algorithm) { "sha256" }
|
232
|
+
|
233
|
+
let(:input) do
|
234
|
+
{
|
235
|
+
:credentials_lookup => credentials_lookup,
|
236
|
+
:method => 'GET',
|
237
|
+
:path => "/resource/4?a=1&bewit=#{bewit}&b=2",
|
238
|
+
:host => 'example.com',
|
239
|
+
:port => 80,
|
240
|
+
}
|
241
|
+
end
|
242
|
+
|
243
|
+
let(:bewit) { "MTIzNDU2XDQ1MTkzMTE0NThcYkkwanFlS1prUHE0V1hRMmkxK0NrQ2lOanZEc3BSVkNGajlmbElqMXphWT1cc29tZS1hcHAtZGF0YQ" }
|
244
|
+
|
245
|
+
let(:now) { 1365711458 }
|
246
|
+
before do
|
247
|
+
Time.stubs(:now).returns(Time.at(now))
|
248
|
+
end
|
249
|
+
|
250
|
+
context "when valid" do
|
251
|
+
it "returns credentials" do
|
252
|
+
expect(described_class.authenticate_bewit(bewit, input)).to eql(credentials)
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
context "when invalid format" do
|
257
|
+
let(:bewit) { "invalid-bewit" }
|
258
|
+
|
259
|
+
it "returns error object" do
|
260
|
+
actual = described_class.authenticate_bewit(bewit, input)
|
261
|
+
expect(actual).to be_a(Hawk::AuthenticationFailure)
|
262
|
+
expect(actual.key).to eql(:id)
|
263
|
+
expect(actual.message).to_not eql(nil)
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
context "when invalid ext" do
|
268
|
+
let(:bewit) { "MTIzNDU2XDQ1MTkzMTE0NThcVTN4dVF5TEVXUGNOa3Q4Vm5oRy9BSDg4VERQZXlKT2JKeGVNb0tkZWZUQT1caW52YWxpZCBleHQ" }
|
269
|
+
|
270
|
+
it "returns error object" do
|
271
|
+
actual = described_class.authenticate_bewit(bewit, input)
|
272
|
+
expect(actual).to be_a(Hawk::AuthenticationFailure)
|
273
|
+
expect(actual.key).to eql(:bewit)
|
274
|
+
expect(actual.message).to_not eql(nil)
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
context "when stale timestamp" do
|
279
|
+
let(:now) { 4519311459 }
|
280
|
+
|
281
|
+
it "returns error object" do
|
282
|
+
actual = described_class.authenticate_bewit(bewit, input)
|
283
|
+
expect(actual).to be_a(Hawk::AuthenticationFailure)
|
284
|
+
expect(actual.key).to eql(:ts)
|
285
|
+
expect(actual.message).to_not eql(nil)
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
2
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
3
|
+
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'hawk'
|
6
|
+
|
7
|
+
RSpec.configure do |config|
|
8
|
+
config.mock_with :mocha
|
9
|
+
config.expect_with :rspec do |c|
|
10
|
+
c.syntax = :expect
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
shared_examples "an authorization header builder" do
|
2
|
+
returns_valid_authorization_header = proc do
|
3
|
+
it "returns valid authorization header" do
|
4
|
+
actual = described_class.build_authorization_header(input)
|
5
|
+
|
6
|
+
expected_output_parts.each do |expected_part|
|
7
|
+
matcher = Regexp === expected_part ? expected_part : Regexp.new(Regexp.escape(expected_part))
|
8
|
+
expect(actual).to match(matcher)
|
9
|
+
end
|
10
|
+
|
11
|
+
expect(actual).to eql(expected_output)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
context "with full options", &returns_valid_authorization_header
|
16
|
+
|
17
|
+
context "without ext" do
|
18
|
+
before do
|
19
|
+
input.delete(:ext)
|
20
|
+
end
|
21
|
+
context '', &returns_valid_authorization_header
|
22
|
+
end
|
23
|
+
|
24
|
+
context "without payload" do
|
25
|
+
before do
|
26
|
+
input.delete(:payload)
|
27
|
+
end
|
28
|
+
context '', &returns_valid_authorization_header
|
29
|
+
end
|
30
|
+
|
31
|
+
context "without ts" do
|
32
|
+
before do
|
33
|
+
input.delete(:ts)
|
34
|
+
end
|
35
|
+
context '', &returns_valid_authorization_header
|
36
|
+
end
|
37
|
+
|
38
|
+
%w( method path host port ).each do |missing_option|
|
39
|
+
context "when missing #{missing_option} option" do
|
40
|
+
before do
|
41
|
+
input.delete(missing_option.to_sym)
|
42
|
+
end
|
43
|
+
|
44
|
+
it "raises MissingOptionError" do
|
45
|
+
expect { described_class.build_authorization_header(input) }.to raise_error(Hawk::AuthorizationHeader::MissingOptionError)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
context "with invalid credentials" do
|
51
|
+
context "when missing id" do
|
52
|
+
before do
|
53
|
+
credentials.delete(:id)
|
54
|
+
end
|
55
|
+
|
56
|
+
it "raises InvalidCredentialsError" do
|
57
|
+
expect { described_class.build_authorization_header(input) }.to raise_error(Hawk::AuthorizationHeader::InvalidCredentialsError)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
context "when missing key" do
|
62
|
+
before do
|
63
|
+
credentials.delete(:key)
|
64
|
+
end
|
65
|
+
|
66
|
+
it "raises InvalidCredentialsError" do
|
67
|
+
expect { described_class.build_authorization_header(input) }.to raise_error(Hawk::AuthorizationHeader::InvalidCredentialsError)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
context "when missing algorithm" do
|
72
|
+
before do
|
73
|
+
credentials.delete(:algorithm)
|
74
|
+
end
|
75
|
+
|
76
|
+
it "raises InvalidCredentialsError" do
|
77
|
+
expect { described_class.build_authorization_header(input) }.to raise_error(Hawk::AuthorizationHeader::InvalidCredentialsError)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
context "when invalid algorithm" do
|
82
|
+
before do
|
83
|
+
credentials[:algorithm] = 'foobar'
|
84
|
+
end
|
85
|
+
|
86
|
+
it "raises InvalidAlgorithmError" do
|
87
|
+
expect { described_class.build_authorization_header(input) }.to raise_error(Hawk::AuthorizationHeader::InvalidAlgorithmError)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
shared_examples "an authorization header authenticator" do
|
94
|
+
context "with valid authorization header" do
|
95
|
+
it "returns credentials object" do
|
96
|
+
expect(described_class.authenticate(authorization_header, input)).to eql(credentials)
|
97
|
+
end
|
98
|
+
|
99
|
+
context "when hash present" do
|
100
|
+
let(:payload) { 'something to write about' }
|
101
|
+
|
102
|
+
it "returns credentials object" do
|
103
|
+
expect(described_class.authenticate(authorization_header, input)).to eql(credentials)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
context "when ext present" do
|
108
|
+
let(:ext) { 'some random ext' }
|
109
|
+
|
110
|
+
it "returns credentials object" do
|
111
|
+
expect(described_class.authenticate(authorization_header, input)).to eql(credentials)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
context "with invalid authorization header" do
|
117
|
+
context "when invalid mac" do
|
118
|
+
let(:expected_mac) { 'foobar' }
|
119
|
+
|
120
|
+
it "returns error object" do
|
121
|
+
actual = described_class.authenticate(authorization_header, input)
|
122
|
+
expect(actual).to be_a(Hawk::AuthenticationFailure)
|
123
|
+
expect(actual.key).to eql(:mac)
|
124
|
+
expect(actual.message).to_not eql(nil)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
context "when invalid hash" do
|
129
|
+
let(:expected_hash) { 'foobar' }
|
130
|
+
let(:payload) { 'baz' }
|
131
|
+
|
132
|
+
it "returns error object" do
|
133
|
+
actual = described_class.authenticate(authorization_header, input)
|
134
|
+
expect(actual).to be_a(Hawk::AuthenticationFailure)
|
135
|
+
expect(actual.key).to eql(:hash)
|
136
|
+
expect(actual.message).to_not eql(nil)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
context "when invalid ext" do
|
141
|
+
before do
|
142
|
+
client_input[:ext] = 'something else'
|
143
|
+
end
|
144
|
+
|
145
|
+
it "returns error object" do
|
146
|
+
actual = described_class.authenticate(authorization_header, input)
|
147
|
+
expect(actual).to be_a(Hawk::AuthenticationFailure)
|
148
|
+
expect(actual.key).to eql(:mac)
|
149
|
+
expect(actual.message).to_not eql(nil)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|