Skip to content

Instantly share code, notes, and snippets.

@bowsersenior
Created November 20, 2012 02:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bowsersenior/4115493 to your computer and use it in GitHub Desktop.
Save bowsersenior/4115493 to your computer and use it in GitHub Desktop.
require 'openssl'
require 'base64'
require 'uri'
# inspired by Amazon's AWS auth (also used by Twilio)
# see:
# * http://www.thebuzzmedia.com/designing-a-secure-rest-api-without-oauth-authentication/
# * https://github.com/amazonwebservices/aws-sdk-for-ruby/blob/master/lib/aws/core/signer.rb
# * https://github.com/amazonwebservices/aws-sdk-for-ruby/blob/master/lib/aws/core/signature/version_4.rb
# * https://github.com/twilio/twilio-ruby/blob/master/lib/twilio-ruby/util/request_validator.rb
#
# Usage:
# sda = SimpleDigestAuth.new :api_key => '123'
#
# sda.sign(
# :request_method => 'GET',
# :body => "",
# :path => '/',
# :query_string => 'foo=bar'
# )
# # => "q5o3wVHT1MFlzjViCKi5ZBEbx/aVu0OgFrL707FUAZQ="
#
# sda.valid_signature?(
# :request_method => 'GET',
# :body => "",
# :path => '/',
# :query_string => 'foo=bar',
# :signature => "q5o3wVHT1MFlzjViCKi5ZBEbx/aVu0OgFrL707FUAZQ="
# )
# # => true
class SimpleDigestAuth
module ArgumentsHelper
def require!(opts, *required_options)
raise "Must pass a hash of options" unless opts.is_a? Hash
missing_options = required_options - opts.keys
raise "Missing required option(s):#{missing_options}" unless missing_options.empty?
end
end
include ArgumentsHelper
attr_accessor :api_key
def initialize(opts={})
require! opts, :api_key
self.api_key = opts[:api_key]
end
def sign(opts={})
require! opts, :request_method, :body, :path, :query_string
cr = self.class.canonical_request_for(opts)
self.class.build_signature_for(
:canonical_request => cr,
:api_key => self.api_key
)
end
def valid_signature?(opts={})
require! opts, :request_method, :body, :path, :query_string, :signature
received_signature = opts.delete(:signature)
calculated_signature = self.sign(opts)
received_signature == calculated_signature
end
private
class << self
include ArgumentsHelper
def canonical_request_for(opts={})
require! opts, :request_method, :body, :path, :query_string
[
opts[:request_method],
opts[:body], # POST params
opts[:path],
opts[:query_string] # GET params
].join("\n")
end
def build_signature_for(opts={})
digest = OpenSSL::Digest::Digest.new('sha256')
Base64.encode64(
OpenSSL::HMAC.digest(digest, opts[:api_key], opts[:canonical_request])
).strip
end
end
end
# This is a plain rack application...
# ...it is very simple!
# For a nice intro, see:
# * http://rubylearning.com/blog/a-quick-introduction-to-rack/
require 'openssl'
require 'base64'
# inspired by Amazon's AWS auth (also used by Twilio)
# see:
# * http://www.thebuzzmedia.com/designing-a-secure-rest-api-without-oauth-authentication/
# * https://github.com/amazonwebservices/aws-sdk-for-ruby/blob/master/lib/aws/core/signer.rb
# * https://github.com/amazonwebservices/aws-sdk-for-ruby/blob/master/lib/aws/core/signature/version_4.rb
# * https://github.com/twilio/twilio-ruby/blob/master/lib/twilio-ruby/util/request_validator.rb
module SimpleRackAuth
class << self
attr_accessor :api_key
end
def canonical_request_for(request_method, path, query_string, body)
[
request_method,
path,
query_string, # GET params
body # POST params
].join("\n")
end
def build_signature_for(canonical_request, api_key)
digest = OpenSSL::Digest::Digest.new('sha256')
Base64.encode64(
OpenSSL::HMAC.digest(digest, api_key, canonical_request)
).strip
end
def valid_signature?(received_signature, canonical_request)
calculated_signature = build_signature_for(
canonical_request,
SimpleRackAuth.api_key
)
received_signature == calculated_signature
end
module_function :build_signature_for, :canonical_request_for, :valid_signature?
# Rack middleware to implement the auth solution
# Usage:
#
# use SimpleRackAuth::Middleware
class Middleware
include SimpleRackAuth
def initialize(app)
@app = app
end
def call(env)
canonical_request = canonical_request_for(
env['REQUEST_METHOD'],
env['REQUEST_PATH'],
env['QUERY_STRING'],
env['rack.input'].read
)
received_signature = env['HTTP_X_SIMPLE_RACK_AUTH_SIGNATURE']
if valid_signature?(received_signature, canonical_request)
status, headers, body = @app.call(env)
headers['X-SimpleRackAuth-Authorized-Canonical-Request'] = canonical_request
headers['X-SimpleRackAuth-Authorized-Signature'] = received_signature
[status, headers, body]
else
[401, {'Content-type' => 'text/plain'}, ['Unauthorized']]
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment