Last active
December 27, 2015 07:59
-
-
Save mtgrosser/7293409 to your computer and use it in GitHub Desktop.
Basic "out of band" implementation of the OAuth 1.0 signature mechanism, inspired by 'net-http-oauth', 'oauth' and 'simple_oauth' gems. o = OoAuth.new
r = Net::HTTP::Get.new('/1.1/statuses/user_timeline.json?screen_name=foobar')
http = Net::HTTP.new('api.twitter.com', Net::HTTP.https_default_port)
http.use_ssl = true o.sign! http, r # will add t…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'uri' | |
require 'net/http' | |
require 'openssl' | |
require 'base64' | |
class OoAuth | |
# request tokens are passed between the consumer and the provider out of | |
# band (i.e. callbacks cannot be used), per section 6.1.1 | |
OUT_OF_BAND = "oob" | |
# required parameters, per sections 6.1.1, 6.3.1, and 7 | |
PARAMETERS = %w(callback consumer_key token signature_method timestamp nonce verifier version signature body_hash) | |
# reserved character regexp, per section 5.1 | |
RESERVED_CHARACTERS = /[^a-zA-Z0-9\-\.\_\~]/ | |
attr_accessor :consumer_key, :consumer_secret, :token, :token_secret | |
class << self | |
# Parse an Authorization / WWW-Authenticate header into a hash. Takes care of unescaping and | |
# removing surrounding quotes. Raises a OAuth::Problem if the header is not parsable into a | |
# valid hash. Does not validate the keys or values. | |
# | |
# hash = parse(headers['Authorization'] || headers['WWW-Authenticate']) | |
# hash['oauth_timestamp'] | |
# #=>"1234567890" | |
# | |
def parse(header) | |
# decompose | |
params = header[6, header.length].split(/[,=&]/) | |
# odd number of arguments - must be a malformed header. | |
raise "Invalid authorization header" if params.size % 2 != 0 | |
params.map! do |v| | |
# strip and unescape | |
val = unescape(v.strip) | |
# strip quotes | |
val.sub(/^\"(.*)\"$/, '\1') | |
end | |
# convert into a Hash and remove non-OAuth parameters | |
Hash[*params.flatten].reject { |k,v| !PARAMETERS.include?(k) } | |
end | |
# Generate a random key of up to +size+ bytes. The value returned is Base64 encoded with non-word | |
# characters removed. | |
def generate_key(size = 32) | |
Base64.encode64(OpenSSL::Random.random_bytes(size)).gsub(/\W/, '') | |
end | |
alias_method :generate_nonce, :generate_key | |
# Escape +value+ by URL encoding all non-reserved character. | |
# | |
# See Also: {OAuth core spec version 1.0, section 5.1}[http://oauth.net/core/1.0#rfc.section.5.1] | |
def escape(value) | |
URI.escape(value.to_s, OAuth::RESERVED_CHARACTERS) | |
rescue ArgumentError | |
URI.escape(value.to_s.force_encoding(Encoding::UTF_8), OAuth::RESERVED_CHARACTERS) | |
end | |
def unescape(value) | |
URI.unescape(value.gsub('+', '%2B')) | |
end | |
# cf. http://tools.ietf.org/html/rfc5849#section-3.4.1.1 | |
# cf. http://tools.ietf.org/html/rfc5849#section-3.4.4 | |
def encode(*components) | |
components.map { |component| escape(component) }.join('&') | |
end | |
def plaintext_signature(consumer_secret, token_secret) | |
encode(consumer_secret, token_secret) | |
end | |
def hmac_sha1_signature(base_string, consumer_secret, token_secret) | |
Base64.strict_encode64(OpenSSL::HMAC.digest(OpenSSL::Digest::SHA1.new, encode(consumer_secret, token_secret), base_string)) | |
end | |
def rsa_sha1_signature(base_string, consumer_secret) | |
Base64.strict_encode64(private_key(consumer_secret).sign(OpenSSL::Digest::SHA1.new, base_string)) | |
end | |
end | |
def initialize | |
@consumer_key, @consumer_secret, @token, @token_secret = 'ck', 'cs', 'at', 'as' | |
end | |
def signature_method | |
'HMAC-SHA1' | |
end | |
def sign!(http, request, options = {}) | |
params = { | |
oauth_version: '1.0', | |
oauth_nonce: self.class.generate_nonce, | |
oauth_timestamp: Time.now.to_i, | |
oauth_signature_method: signature_method, | |
oauth_consumer_key: consumer_key, | |
oauth_token: token | |
} | |
params[:oauth_signature] = case signature_method | |
when 'PLAINTEXT' | |
self.class.plaintext_signature(consumer_secret, token_secret) | |
when 'HMAC-SHA1' | |
self.class.hmac_sha1_signature(signature_base_string(http, request, params), consumer_secret, token_secret) | |
when 'RSA-SHA1' | |
self.class.rsa_sha1_signature(signature_base_string(http, request, params), consumer_secret) | |
else | |
raise "error: signature method not supported: #{signature_method}" | |
end | |
request['Authorization'] = authorization_header(params) | |
end | |
def valid?(http, response, options = {}) | |
return false | |
consumer_secret = options.fetch(:consumer_secret) | |
signature_method = options.fetch(:signature_method) { 'HMAC-SHA1' } | |
token_secret = options[:token_secret] | |
params = parse(response.headers['Authorization']) | |
end | |
private | |
def signature_base_string(http, request, params = {}) | |
encoded_params = params_encode(params_array(request) + params_array(params)) | |
self.class.encode(request.method, normalized_request_uri(http, request), encoded_params) | |
end | |
# FIXME: cf nested params implementation in oauth gem | |
def params_array(object) | |
case object | |
when Array then return object | |
when Hash then return object.to_a | |
when Net::HTTPRequest | |
tmp = object.path.split('?') | |
tmp[1] ? params_decode(tmp[1]) : [] | |
else | |
raise "error: cannot convert #{object.class} object to params array" | |
end | |
end | |
def params_decode(string) | |
string.split('&').each_with_object([]) do |param, array| | |
k, v = *param.split('=') | |
array << [self.class.unescape(k), v && self.class.unescape(v)] | |
end | |
end | |
# cf. http://tools.ietf.org/html/rfc5849#section-3.4.1.3.2 | |
def params_encode(params) | |
params.map { |k, v| [self.class.escape(k), self.class.escape(v)] }.sort.map { |k, v| "#{k}=#{v}" }.join('&') | |
end | |
def normalized_request_uri(http, request) | |
if http.port == Net::HTTP.default_port | |
scheme, port = :http, nil | |
elsif http.port == Net::HTTP.https_default_port | |
scheme, port = :https, nil | |
elsif http.use_ssl? | |
scheme, port = :https, http.port | |
else | |
scheme, port = :http, http.port | |
end | |
uri = "#{scheme}://#{http.address.downcase}" | |
uri += ":#{port}" if port | |
uri += request.path.split('?').first | |
uri | |
end | |
def authorization_header(params) | |
'OAuth ' + params.map { |k, v| "#{self.class.escape(k)}=\"#{self.class.escape(v)}\"" }.join(', ') | |
end | |
def private_key(object) | |
# OpenSSL::PKey::RSA === object ? object : OpenSSL::PKey::RSA.new(IO.read(object)) | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment