Permalink
Browse files

Initial implementation

  • Loading branch information...
0 parents commit 707e21b1817dae4b38415b97b276993da57822ee @skade skade committed Mar 31, 2011
Showing with 289 additions and 0 deletions.
  1. +8 −0 Gemfile
  2. +28 −0 Gemfile.lock
  3. +21 −0 LICENSE
  4. +46 −0 README.md
  5. +10 −0 Rakefile
  6. +19 −0 hmac.gemspec
  7. +33 −0 lib/hmac.rb
  8. +51 −0 lib/hmac_strategy.rb
  9. +40 −0 test/hmac_strategy_test.rb
  10. +33 −0 test/test_helper.rb
@@ -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"
@@ -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 LICENSE
@@ -0,0 +1,21 @@
+Copyright (c) 2011 Florian Gilcher <florian.gilcher@asquera.de>
+
+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.
+
@@ -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
@@ -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
@@ -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 = ["florian.gilcher@asquera.de"]
+ 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
@@ -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
+
@@ -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)
@@ -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
@@ -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.