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 ADDED
@@ -0,0 +1,7 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 1.9.2
5
+ - rbx-19mode
6
+ - 1.8.7
7
+ - ree
data/README.md CHANGED
@@ -1,74 +1,7 @@
1
- # Hawk
1
+ # Hawk [![Build Status](https://travis-ci.org/tent/hawk-ruby.png)](https://travis-ci.org/tent/hawk-ruby)
2
2
 
3
3
  Ruby implementation of [Hawk HTTP authentication scheme](https://github.com/hueniverse/hawk).
4
4
 
5
- **Authorization Request Header**
6
-
7
- ```
8
- Authorization: Hawk id="{credentials id}", ts="{epoch timestamp}", nonce="{nonce}", hash="{hash}", ext="{ext}", mac="{mac}", app="{application id}", d1g="{d1g}"
9
- ```
10
-
11
- `hash`, `ext`, `app`, and `d1g` should only be included if used in mac function.
12
-
13
- **Authorization Response Header**
14
-
15
- ```
16
- Server-Authorization: Hawk mac="{mac}", hash="{hash}", ext="{ext}"
17
- ```
18
-
19
- `mac` is constructed using the same params as in the request with the exception of `hash` and `ext` which are replaced with new values.
20
-
21
- `hash` and `ext` are both optional.
22
-
23
- **MAC Function**
24
-
25
- ```
26
- base-64(
27
- hmac-{algorithm (e.g. sha-256)}(
28
- hawk.{hawk version}.{type}
29
- {epoch timestamp}
30
- {nonce}
31
- {uppercase request method}
32
- {lowercase request path}
33
- {lowercase request host}
34
- {request port}
35
- {hash (see below) or empty line}
36
- {ext (optional)}
37
- {application id (optional)}
38
- {application id digest (requires application id)}
39
- )
40
- )
41
- ```
42
-
43
- **Payload Hash Function**
44
-
45
- ```
46
- base-64(
47
- digest-{algorithm (e.g. sha-256)}(
48
- hawk.#{hawk version}.payload
49
- {plain content-type (e.g. application/json)}
50
- {request payload or empty line}
51
- )
52
- )
53
- ```
54
-
55
- **Bewit MAC Function**
56
-
57
- ```
58
- base-64(
59
- {credentials id} + \ + {expiry epoch timestamp} + \ + hmac-{algorithm (e.g. sha-256)}(
60
- hawk.{hawk version}.bewit
61
- {epoch timestamp}
62
- {nonce}
63
- {uppercase request method}
64
- {lowercase request path}
65
- {lowercase request host}
66
- {request port}
67
- {ext (optional)}
68
- ) + \ + {ext or empty}
69
- )
70
- ```
71
-
72
5
  ## Installation
73
6
 
74
7
  Add this line to your application's Gemfile:
@@ -85,7 +18,96 @@ Or install it yourself as:
85
18
 
86
19
  ## Usage
87
20
 
88
- TODO: write usage instructions
21
+ ```
22
+ $ irb
23
+ > require 'hawk'
24
+
25
+ > Hawk::Client.build_authorization_header(
26
+ > :credentials => {
27
+ > :id => '123456',
28
+ > :key => '2983d45yun89q',
29
+ > :algorithm => 'sha256'
30
+ > },
31
+ > :ts => 1365898519,
32
+ > :method => 'POST',
33
+ > :path => '/somewhere/over/the/rainbow',
34
+ > :host => 'example.net',
35
+ > :port => 80,
36
+ > :payload => 'something to write about',
37
+ > :ext => 'Bazinga!',
38
+ > :nonce => 'Ygvqdz'
39
+ > )
40
+ Hawk id="123456", ts="1365898519", nonce="Ygvqdz", hash="LjRmtkSKTW0ObTUyZ7N+vjClKd//KTTdfhF1M4XCuEM=", ext="Bazinga!", mac="07uWxZfesjgR9wGYXMfCPvocryS9ct8Ir6/83zj3A5s="
41
+
42
+ > Hawk::Client.authenticate(
43
+ > %(Hawk hash="2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=", mac="0ysNmHEhwCjww5yQdbVZ1yXQ58CiRkc8O3l+rSk/TZE="),
44
+ > :credentials => {
45
+ > :id => "123456",
46
+ > :key => "2983d45yun89q",
47
+ > :algorithm => "sha256"
48
+ > },
49
+ > :ts => 1365899773,
50
+ > :method => "POST",
51
+ > :path => "/somewhere/over/the/rainbow",
52
+ > :host => "example.net",
53
+ > :port => 80,
54
+ > :payload => "something to write about",
55
+ > :content_type => "text/plain",
56
+ > :nonce => "Ygvqdz",
57
+ > )
58
+ { :id => "123456", :key => "2983d45yun89q", :algorithm => "sha256" }
59
+
60
+ > Hawk::Client.calculate_time_offset(
61
+ > %(Hawk ts="1365741469", tsm="h/Ff6XI1euObD78ZNflapvLKXGuaw1RiLI4Q6Q5sAbM=", error="Some Error Message"),
62
+ > :credentials => { :id => "123456", :key => "2983d45yun89q", :algorithm => "sha256" }
63
+ > )
64
+ 321
65
+
66
+ > credentials = { :id => "123456", :key => "2983d45yun89q", :algorithm => "sha256" }
67
+ { :id => "123456", :key => "2983d45yun89q", :algorithm => "sha256" }
68
+ > Hawk::Server.authenticate(
69
+ > %(Hawk id="123456", ts="1365900371", nonce="Ygvqdz", hash="9LxQVpfaAgyiyNeOgD8TEKP6RnM=", mac="lv54INsJZym8wnME0nQAu5jW6BA="),
70
+ > :method => "POST",
71
+ > :path => "/somewhere/over/the/rainbow",
72
+ > :host => "example.net",
73
+ > :port => 80,
74
+ > :content_type => "text/plain",
75
+ > :credentials_lookup => lambda { |id| id == credentials[:id] ? credentials : nil },
76
+ > :nonce_lookup => lambda { |nonce| },
77
+ > :payload => "something to write about"
78
+ > )
79
+ { :id => "123456", :key => "2983d45yun89q", :algorithm => "sha256" }
80
+
81
+ > res = Hawk::Server.authenticate(
82
+ > %(Hawk id="123456", ts="1365901299", mac="zTu3FSTmdsdSaLHd/DrpeQRkuYzcb0snYYKOmwDwP3w="),
83
+ > :method => "POST",
84
+ > :path => "/somewhere/over/the/rainbow",
85
+ > :host => "example.net",
86
+ > :port => 80,
87
+ > :content_type => "text/plain",
88
+ > :credentials_lookup => lambda { |id| id == credentials[:id] ? credentials : nil },
89
+ > :nonce_lookup => lambda { |nonce| }
90
+ > )
91
+ #<Hawk::AuthenticationFailure:0x007f95cba33168 @key=:nonce, @message="Missing nonce", @options={:credentials=>{:id=>"123456", :key=>"2983d45yun89q", :algorithm=>"sha256"}}>
92
+ > res.header
93
+ Hawk ts="1365901388", tsm="6mdH5DT66UeWlkBC9x2QD7Upt0eYnud9dB7y7xKoEoU=", error="Missing nonce"
94
+
95
+ > Hawk::Server.build_authorization_header(
96
+ > :credentials => {
97
+ > :id => "123456",
98
+ > :key => "2983d45yun89q",
99
+ > :algorithm => "sha1"
100
+ > },
101
+ > :ts => 1365900682,
102
+ > :method => "POST",
103
+ > :path => "/somewhere/over/the/rainbow",
104
+ > :host => "example.net",
105
+ > :port => 80,
106
+ > :ext => "Bazinga!",
107
+ > :nonce => "Ygvqdz"
108
+ > )
109
+ Hawk ext="Bazinga!", mac="5D0CgZEXKEdeUFYbE5HQqb7ZooI="
110
+ ```
89
111
 
90
112
  ## Contributing
91
113
 
data/Rakefile CHANGED
@@ -1 +1,8 @@
1
- require "bundler/gem_tasks"
1
+ require 'bundler/setup'
2
+ require 'bundler/gem_tasks'
3
+
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new(:spec) do |spec|
6
+ spec.pattern = 'spec/**/*_spec.rb'
7
+ end
8
+ task :default => :spec
data/hawk-auth.gemspec CHANGED
@@ -19,4 +19,6 @@ Gem::Specification.new do |gem|
19
19
 
20
20
  gem.add_development_dependency "bundler", "~> 1.3"
21
21
  gem.add_development_dependency "rake"
22
+ gem.add_development_dependency 'rspec', '~> 2.11'
23
+ gem.add_development_dependency 'mocha', '~> 0.13'
22
24
  end
data/lib/hawk.rb CHANGED
@@ -1,6 +1,9 @@
1
1
  require 'hawk/version'
2
2
 
3
3
  module Hawk
4
- NotImplementedError = Class.new(StandardError)
5
- raise NotImplementedError.new("This implementation of hawk is currently empty!")
4
+ require 'hawk/crypto'
5
+ require 'hawk/authentication_failure'
6
+ require 'hawk/authorization_header'
7
+ require 'hawk/client'
8
+ require 'hawk/server'
6
9
  end
@@ -0,0 +1,18 @@
1
+ module Hawk
2
+ class AuthenticationFailure
3
+ attr_reader :key, :message
4
+ def initialize(key, message, options = {})
5
+ @key, @message, @options = key, message, options
6
+ end
7
+
8
+ def header
9
+ timestamp = Time.now.to_i
10
+ if @options[:credentials]
11
+ timestamp_mac = Crypto.ts_mac(:ts => timestamp, :credentials => @options[:credentials])
12
+ %(Hawk ts="#{timestamp}", tsm="#{timestamp_mac}", error="#{message}")
13
+ else
14
+ %(Hawk error="#{message}")
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,114 @@
1
+ module Hawk
2
+ module AuthorizationHeader
3
+ extend self
4
+
5
+ REQUIRED_OPTIONS = [:method, :path, :host, :port].freeze
6
+ REQUIRED_CREDENTIAL_MEMBERS = [:id, :key, :algorithm].freeze
7
+ SUPPORTED_ALGORITHMS = ['sha256', 'sha1'].freeze
8
+ HEADER_PARTS = [:id, :ts, :nonce, :hash, :ext, :mac].freeze
9
+
10
+ MissingOptionError = Class.new(StandardError)
11
+ InvalidCredentialsError = Class.new(StandardError)
12
+ InvalidAlgorithmError = Class.new(StandardError)
13
+
14
+ def build(options, only=nil)
15
+ options[:ts] ||= Time.now.to_i
16
+ options[:nonce] ||= SecureRandom.hex(4)
17
+
18
+ REQUIRED_OPTIONS.each do |key|
19
+ unless options.has_key?(key)
20
+ raise MissingOptionError.new("#{key.inspect} is missing!")
21
+ end
22
+ end
23
+
24
+ credentials = options[:credentials]
25
+ REQUIRED_CREDENTIAL_MEMBERS.each do |key|
26
+ unless credentials.has_key?(key)
27
+ raise InvalidCredentialsError.new("#{key.inspect} is missing!")
28
+ end
29
+ end
30
+
31
+ unless SUPPORTED_ALGORITHMS.include?(credentials[:algorithm])
32
+ raise InvalidAlgorithmError.new("#{credentials[:algorithm].inspect} is not a supported algorithm! Use one of the following: #{SUPPORTED_ALGORITHMS.join(', ')}")
33
+ end
34
+
35
+ hash = Crypto.hash(options)
36
+ mac = Crypto.mac(options)
37
+
38
+ parts = {
39
+ :id => credentials[:id],
40
+ :ts => options[:ts],
41
+ :nonce => options[:nonce],
42
+ :mac => mac
43
+ }
44
+ parts[:hash] = hash if options.has_key?(:payload)
45
+ parts[:ext] = options[:ext] if options.has_key?(:ext)
46
+
47
+ "Hawk " << (only || HEADER_PARTS).inject([]) { |memo, key|
48
+ next memo unless parts.has_key?(key)
49
+ memo << %(#{key}="#{parts[key]}")
50
+ memo
51
+ }.join(', ')
52
+ end
53
+
54
+ def authenticate(header, options)
55
+ parts = parse(header)
56
+
57
+ now = Time.now.to_i
58
+
59
+ if options[:server_response]
60
+ credentials = options[:credentials]
61
+ parts.merge!(
62
+ :ts => options[:ts],
63
+ :nonce => options[:nonce]
64
+ )
65
+ else
66
+ unless options[:credentials_lookup].respond_to?(:call) && (credentials = options[:credentials_lookup].call(parts[:id]))
67
+ return AuthenticationFailure.new(:id, "Unidentified id")
68
+ end
69
+
70
+ if (now - parts[:ts].to_i > 1000) || (parts[:ts].to_i - now > 1000)
71
+ # Stale timestamp
72
+ return AuthenticationFailure.new(:ts, "Stale ts", :credentials => credentials)
73
+ end
74
+
75
+ unless parts[:nonce]
76
+ return AuthenticationFailure.new(:nonce, "Missing nonce")
77
+ end
78
+
79
+ if options[:nonce_lookup].respond_to?(:call) && options[:nonce_lookup].call(parts[:nonce])
80
+ # Replay
81
+ return AuthenticationFailure.new(:nonce, "Invalid nonce")
82
+ end
83
+ end
84
+
85
+ expected_mac = Crypto.mac(options.merge(
86
+ :credentials => credentials,
87
+ :ts => parts[:ts],
88
+ :nonce => parts[:nonce],
89
+ :ext => parts[:ext]
90
+ ))
91
+ unless expected_mac == parts[:mac]
92
+ return AuthenticationFailure.new(:mac, "Invalid mac")
93
+ end
94
+
95
+ expected_hash = parts[:hash] ? Crypto.hash(options.merge(:credentials => credentials)) : nil
96
+ if expected_hash && expected_hash != parts[:hash]
97
+ return AuthenticationFailure.new(:hash, "Invalid hash")
98
+ end
99
+
100
+ credentials
101
+ end
102
+
103
+ def parse(header)
104
+ parts = header.sub(/\AHawk\s+/, '').split(/,\s*/)
105
+ parts.inject(Hash.new) do |memo, part|
106
+ next memo unless part =~ %r{([a-z]+)=(['"])([^\2]+)\2}
107
+ key, val = $1, $3
108
+ memo[key.to_sym] = val
109
+ memo
110
+ end
111
+ end
112
+
113
+ end
114
+ end
@@ -0,0 +1,22 @@
1
+ require 'securerandom'
2
+
3
+ module Hawk
4
+ module Client
5
+ extend self
6
+
7
+ def authenticate(authorization_header, options)
8
+ Hawk::AuthorizationHeader.authenticate(authorization_header, { :server_response => true }.merge(options))
9
+ end
10
+
11
+ def build_authorization_header(options)
12
+ Hawk::AuthorizationHeader.build(options)
13
+ end
14
+
15
+ def calculate_time_offset(authorization_header, options)
16
+ parts = AuthorizationHeader.parse(authorization_header)
17
+ expected_mac = Crypto.ts_mac(:ts => parts[:ts], :credentials => options[:credentials])
18
+ return unless expected_mac == parts[:tsm]
19
+ parts[:ts].to_i - Time.now.to_i
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,96 @@
1
+ require 'base64'
2
+ require 'openssl'
3
+
4
+ ##
5
+ # Ruby 1.8.7 compatibility
6
+ unless Base64.respond_to?(:strict_encode64)
7
+ Base64.class_eval do
8
+ def strict_encode64(bin)
9
+ [bin].pack("m0")
10
+ end
11
+ end
12
+ end
13
+ unless Base64.respond_to?(:urlsafe_encode64)
14
+ Base64.class_eval do
15
+ def urlsafe_encode64(bin)
16
+ strict_encode64(bin).tr("+/", "-_").gsub("\n", '')
17
+ end
18
+ end
19
+ end
20
+
21
+ module Hawk
22
+ module Crypto
23
+ extend self
24
+
25
+ def hash(options)
26
+ parts = []
27
+
28
+ parts << "hawk.1.payload"
29
+ parts << options[:content_type]
30
+ parts << options[:payload].to_s
31
+ parts << nil # trailing newline
32
+
33
+ Base64.encode64(OpenSSL::Digest.const_get(options[:credentials][:algorithm].upcase).digest(parts.join("\n"))).chomp
34
+ end
35
+
36
+ def normalized_string(options)
37
+ parts = []
38
+
39
+ parts << "hawk.1.#{options[:type] || 'header'}"
40
+ parts << options[:ts]
41
+ parts << options[:nonce]
42
+ parts << options[:method].to_s.upcase
43
+ parts << options[:path]
44
+ parts << options[:host]
45
+ parts << options[:port]
46
+ parts << options[:hash]
47
+ parts << options[:ext]
48
+ parts << nil # trailing newline
49
+
50
+ parts.join("\n")
51
+ end
52
+
53
+ def mac(options)
54
+ if !options[:hash] && options.has_key?(:payload)
55
+ options[:hash] = hash(options)
56
+ end
57
+
58
+ Base64.encode64(
59
+ OpenSSL::HMAC.digest(
60
+ openssl_digest(options[:credentials][:algorithm]).new,
61
+ options[:credentials][:key],
62
+ normalized_string(options)
63
+ )
64
+ ).chomp
65
+ end
66
+
67
+ def ts_mac(options)
68
+ Base64.encode64(
69
+ OpenSSL::HMAC.digest(
70
+ openssl_digest(options[:credentials][:algorithm]).new,
71
+ options[:credentials][:key],
72
+ "hawk.1.ts\n#{options[:ts]}\n"
73
+ )
74
+ ).chomp
75
+ end
76
+
77
+ def bewit(options)
78
+ options[:ts] ||= Time.now.to_i + options[:ttl].to_i
79
+
80
+ _mac = mac(options.merge(:type => 'bewit'))
81
+
82
+ parts = []
83
+
84
+ parts << options[:credentials][:id]
85
+ parts << options[:ts]
86
+ parts << _mac
87
+ parts << options[:ext]
88
+
89
+ Base64.urlsafe_encode64(parts.join("\\")).chomp.sub(/=+\Z/, '')
90
+ end
91
+
92
+ def openssl_digest(algorithm)
93
+ OpenSSL::Digest.const_get(algorithm.upcase)
94
+ end
95
+ end
96
+ end