Skip to content

Commit

Permalink
Initial implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
skade committed Mar 31, 2011
0 parents commit 707e21b
Show file tree
Hide file tree
Showing 10 changed files with 289 additions and 0 deletions.
8 changes: 8 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
source :rubygems

gemspec

gem "rack", :group => "test"
gem "rack-test", :require => "rack/test", :group => "test"
gem "warden", :group => "test"
gem "riot", :group => "test"
28 changes: 28 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
PATH
remote: .
specs:
hmac (1.0)
addressable

GEM
remote: http://rubygems.org/
specs:
addressable (2.2.4)
rack (1.2.2)
rack-test (0.5.7)
rack (>= 1.0)
riot (0.12.3)
rr
rr (1.0.2)
warden (1.0.3)
rack (>= 1.0.0)

PLATFORMS
ruby

DEPENDENCIES
hmac!
rack
rack-test
riot
warden
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
Copyright (c) 2011 Florian Gilcher <[email protected]>

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# HMAC

This gem provides a tiny HMAC implementation along with a warden strategy to us it.

## HMAC usage

h = HMAC.new('secret', 'md5')
h.generate_signature('http://example.com/')

If you want to generate the signature for a signed URL, pass the parameter used for the token:

h = HMAC.new('secret', 'md5')
h.generate_signature('http://example.com/?token=123', 'token')

## Warden strategy usage

Configure the HMAC warden strategy:

use Warden::Manager do |manager|
manager.failure_app = -> env { [401, {"Content-Length" => "0"}, [""]] }
# other scopes
manager.scope_defaults :token, :strategies => [:hmac],
:store => false,
:hmac => {
:params => ["user_id"],
:token => "token",
:secret => "secrit",
:algorithm => "md5",
:hmac => HMAC
}
end

`params` allows you to specify parameters the request must contain, `token` is the name of the token parameter, `secret` and `algorithm` allow you to specify
the secret and algorithm used for the HMAC algorithm. `hmac` expects a class that implement the HMAC algorithm. It is instantiated on each request.

If you want to retrieve the secret and token using a different strategy, extend the HMAC strategy:

class Warden::Strategies::HMAC < Warden::Strategies::Base
def retrieve_user
User.get(request[:user_id])
end
def secret
retrieve_user.secret
end
end
10 changes: 10 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
require 'rake/testtask'

Rake::TestTask.new(:test) do |test|
test.libs << 'test'
test.pattern = 'test/**/*_test.rb'
test.verbose = true
test.ruby_opts = ['-rubygems', '-rtest_helper']
end

task :default => :test
19 changes: 19 additions & 0 deletions hmac.gemspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# -*- encoding: utf-8 -*-

Gem::Specification.new do |s|
s.name = "hmac"
s.version = "1.0"
s.platform = Gem::Platform::RUBY
s.authors = ["Florian Gilcher"]
s.email = ["[email protected]"]
s.summary = %q{A tiny HMAC implementation}
s.description = %q{A tiny HMAC implementation in use at Asquera. Also includes
a warden strategy.}

s.files = %w( README.md Rakefile LICENSE )
s.files += Dir.glob("lib/**/*")

s.require_paths = ["lib"]

s.add_runtime_dependency(%q<addressable>)
end
33 changes: 33 additions & 0 deletions lib/hmac.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
require 'cgi'
require 'addressable/uri'
require 'openssl'

class HMAC
attr_accessor :secret, :algorithm

def initialize(secret, algorithm = "md5")
self.secret = secret
self.algorithm = algorithm
end

def generate_signature(url, token = "token")
uri = Addressable::URI.parse(url)
query_values = uri.query_values

return false unless query_values

query_values.delete(token)
uri.query = canonical_querystring(query_values)

OpenSSL::HMAC.hexdigest(algorithm, secret, uri.to_s)
end

def canonical_querystring(params)
params.sort.map do |key, value|
"%{key}=%{value}" % {:key => CGI.escape(key),
:value => CGI.escape(value)}
end.join("&")
end

end

51 changes: 51 additions & 0 deletions lib/hmac_strategy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
require 'hmac'
require 'warden'

class Warden::Strategies::HMAC < Warden::Strategies::Base
def valid?
config[:params].all? { |p| params.include?(p.to_s) } &&
params.include?(config[:token])
end

def authenticate!
given = params[config[:token]]
expected = hmac.generate_signature(request.url, token)

if given == expected
success!(retrieve_user)
else
halt!
end
end

def params
request.GET
end

def retrieve_user
true
end

private
def config
env["warden"].config[:scope_defaults][scope][:hmac]
end

def hmac
config[:hmac].new(secret, algorithm)
end

def algorithm
config[:algorithm]
end

def token
config[:token]
end

def secret
config[:secret]
end
end

Warden::Strategies.add(:hmac, Warden::Strategies::HMAC)
40 changes: 40 additions & 0 deletions test/hmac_strategy_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
require 'hmac_strategy'
require 'rack/builder'

context "HMAC" do
app(
Rack::Builder.new do
use Rack::Session::Cookie
use Warden::Manager do |manager|
manager.failure_app = -> env { [401, {"Content-Length" => "0"}, [""]] }
manager.default_scope = :default
manager.scope_defaults :default, :strategies => [:password, :basic]
manager.scope_defaults :token, :strategies => [:hmac],
:store => false,
:hmac => {
:params => ["user_id"],
:token => "token",
:secret => "secrit",
:algorithm => "md5",
:hmac => HMAC
}
end

run -> env {
env["warden"].authenticate!(:scope => :token)
[200, {"Content-Length" => "0"}, [""]]
}
end.to_app
)

context "app" do
setup do
uri = "http://example.org/?user_id=123"
signed = uri + "&token=" + HMAC.new('secrit','md5').generate_signature(uri)

get signed
end

asserts(:status).equals(200)
end
end
33 changes: 33 additions & 0 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
begin
# Require the preresolved locked set of gems.
require File.expand_path('../../.bundle/environment', __FILE__)
rescue LoadError
# Fallback on doing the resolve at runtime.
require 'rubygems'
require 'bundler'
Bundler.setup
end

Bundler.require(:default, :test)

class Riot::Situation
include Rack::Test::Methods
include Warden::Test::Helpers

def app
@app
end
end

class Riot::Context
# Set the Rack app which is to be tested.
#
# context "MyApp" do
# app { [200, {}, "Hello!"] }
# setup { get '/' }
# asserts(:status).equals(200)
# end
def app(app=nil, &block)
setup { @app = (app || block) }
end
end

0 comments on commit 707e21b

Please sign in to comment.