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/.travis.yml
    ADDED
    
    
    
        data/README.md
    CHANGED
    
    | 
         @@ -1,74 +1,7 @@ 
     | 
|
| 
       1 
     | 
    
         
            -
            # Hawk
         
     | 
| 
      
 1 
     | 
    
         
            +
            # Hawk [](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 
     | 
    
         
            -
             
     | 
| 
      
 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
    
    
    
        data/hawk-auth.gemspec
    CHANGED
    
    
    
        data/lib/hawk.rb
    CHANGED
    
    | 
         @@ -1,6 +1,9 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            require 'hawk/version'
         
     | 
| 
       2 
2 
     | 
    
         | 
| 
       3 
3 
     | 
    
         
             
            module Hawk
         
     | 
| 
       4 
     | 
    
         
            -
               
     | 
| 
       5 
     | 
    
         
            -
               
     | 
| 
      
 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
         
     | 
    
        data/lib/hawk/client.rb
    ADDED
    
    | 
         @@ -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
         
     | 
    
        data/lib/hawk/crypto.rb
    ADDED
    
    | 
         @@ -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
         
     |