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.
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Hawk
2
- VERSION = "0.0.0"
2
+ VERSION = "0.1.0"
3
3
  end
@@ -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
@@ -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
@@ -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