hawk-auth 0.0.0 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|