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 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