lib/oauthenticator/signed_request.rb in oauthenticator-0.1.4 vs lib/oauthenticator/signed_request.rb in oauthenticator-1.0.0
- old
+ new
@@ -1,6 +1,7 @@
-require 'simple_oauth'
+require 'oauthenticator/signable_request'
+require 'oauthenticator/parse_authorization'
module OAuthenticator
# this class represents an OAuth signed request. its primary user-facing method is {#errors}, which returns
# nil if the request is valid and authentic, or a helpful object of error messages describing what was
# invalid if not.
@@ -24,30 +25,31 @@
end
@extended_classes[config_methods_module]
end
end
- ATTRIBUTE_KEYS = %w(request_method url body media_type authorization).map(&:freeze).freeze
- OAUTH_ATTRIBUTE_KEYS = %w(consumer_key token timestamp nonce version signature_method signature).map(&:to_sym).freeze
+ # attributes of a SignedRequest
+ ATTRIBUTE_KEYS = %w(request_method uri body media_type authorization).map(&:freeze).freeze
+ # oauth attributes parsed from the request authorization
+ OAUTH_ATTRIBUTE_KEYS = (SignableRequest::PROTOCOL_PARAM_KEYS + %w(signature body_hash)).freeze
+
# readers
ATTRIBUTE_KEYS.each { |attribute_key| define_method(attribute_key) { @attributes[attribute_key] } }
# readers for oauth header parameters
- OAUTH_ATTRIBUTE_KEYS.each { |key| define_method(key) { oauth_header_params[key] } }
+ OAUTH_ATTRIBUTE_KEYS.each { |key| define_method(key) { oauth_header_params["oauth_#{key}"] } }
# question methods to indicate whether oauth header parameters were included with a non-blank value in
# the Authorization header
OAUTH_ATTRIBUTE_KEYS.each do |key|
define_method("#{key}?") do
- value = oauth_header_params[key]
+ value = oauth_header_params["oauth_#{key}"]
value.is_a?(String) ? !value.empty? : !!value
end
end
- VALID_SIGNATURE_METHODS = %w(HMAC-SHA1 RSA-SHA1 PLAINTEXT).map(&:freeze).freeze
-
class << self
# instantiates a `OAuthenticator::SignedRequest` (subclass thereof, more precisely) representing a
# request given as a Rack::Request.
#
# like {#initialize}, this should be called on a subclass of SignedRequest created with {.including_config}
@@ -55,11 +57,11 @@
# @param request [Rack::Request]
# @return [subclass of OAuthenticator::SignedRequest]
def from_rack_request(request)
new({
:request_method => request.request_method,
- :url => request.url,
+ :uri => request.url,
:body => request.body,
:media_type => request.media_type,
:authorization => request.env['HTTP_AUTHORIZATION'],
})
end
@@ -85,148 +87,157 @@
#
# {'attribute1': ['messageA', 'messageB'], 'attribute2': ['messageC']}
#
# @return [nil, Hash<String, Array<String>>] either nil or a hash of errors
def errors
- @errors ||= begin
+ return @errors if instance_variables.any? { |ivar| ivar.to_s == '@errors' }
+ @errors = catch(:errors) do
if authorization.nil?
- {'Authorization' => ["Authorization header is missing"]}
+ throw(:errors, {'Authorization' => ["Authorization header is missing"]})
elsif authorization !~ /\S/
- {'Authorization' => ["Authorization header is blank"]}
- elsif authorization !~ /\Aoauth\s/i
- {'Authorization' => ["Authorization scheme is not OAuth; received Authorization: #{authorization}"]}
+ throw(:errors, {'Authorization' => ["Authorization header is blank"]})
+ end
+
+ begin
+ oauth_header_params
+ rescue OAuthenticator::Error => parse_exception
+ throw(:errors, parse_exception.errors)
+ end
+
+ errors = Hash.new { |h,k| h[k] = [] }
+
+ # timestamp
+ if !timestamp?
+ unless signature_method == 'PLAINTEXT'
+ errors['Authorization oauth_timestamp'] << "is missing"
+ end
+ elsif timestamp !~ /\A\s*\d+\s*\z/
+ errors['Authorization oauth_timestamp'] << "is not an integer - got: #{timestamp}"
else
- to_rescue = SimpleOAuth.const_defined?(:ParseError) ? SimpleOAuth::ParseError : StandardError
- begin
- oauth_header_params
- rescue to_rescue
- parse_exception = $!
+ timestamp_i = timestamp.to_i
+ if timestamp_i < Time.now.to_i - timestamp_valid_past
+ errors['Authorization oauth_timestamp'] << "is too old: #{timestamp}"
+ elsif timestamp_i > Time.now.to_i + timestamp_valid_future
+ errors['Authorization oauth_timestamp'] << "is too far in the future: #{timestamp}"
end
- if parse_exception
- if parse_exception.class.name == 'SimpleOAuth::ParseError'
- message = parse_exception.message
- else
- message = "Authorization header is not a properly-formed OAuth 1.0 header."
- end
- {'Authorization' => [message]}
- else
- errors = Hash.new { |h,k| h[k] = [] }
+ end
- # timestamp
- if !timestamp?
- errors['Authorization oauth_timestamp'] << "is missing"
- elsif timestamp !~ /\A\s*\d+\s*\z/
- errors['Authorization oauth_timestamp'] << "is not an integer - got: #{timestamp}"
- else
- timestamp_i = timestamp.to_i
- if timestamp_i < Time.now.to_i - timestamp_valid_past
- errors['Authorization oauth_timestamp'] << "is too old: #{timestamp}"
- elsif timestamp_i > Time.now.to_i + timestamp_valid_future
- errors['Authorization oauth_timestamp'] << "is too far in the future: #{timestamp}"
- end
- end
+ # oauth version
+ if version? && version != '1.0'
+ errors['Authorization oauth_version'] << "must be 1.0; got: #{version}"
+ end
- # oauth version
- if version? && version != '1.0'
- errors['Authorization oauth_version'] << "must be 1.0; got: #{version}"
- end
+ # she's filled with secrets
+ secrets = {}
- # she's filled with secrets
- secrets = {}
+ # consumer / client application
+ if !consumer_key?
+ errors['Authorization oauth_consumer_key'] << "is missing"
+ else
+ secrets[:consumer_secret] = consumer_secret
+ if !secrets[:consumer_secret]
+ errors['Authorization oauth_consumer_key'] << 'is invalid'
+ end
+ end
- # consumer / client application
- if !consumer_key?
- errors['Authorization oauth_consumer_key'] << "is missing"
- else
- secrets[:consumer_secret] = consumer_secret
- if !secrets[:consumer_secret]
- errors['Authorization oauth_consumer_key'] << 'is invalid'
- end
- end
+ # token
+ if token?
+ secrets[:token_secret] = token_secret
+ if !secrets[:token_secret]
+ errors['Authorization oauth_token'] << 'is invalid'
+ elsif !token_belongs_to_consumer?
+ errors['Authorization oauth_token'] << 'does not belong to the specified consumer'
+ end
+ end
- # access token
- if token?
- secrets[:token_secret] = access_token_secret
- if !secrets[:token_secret]
- errors['Authorization oauth_token'] << 'is invalid'
- elsif !access_token_belongs_to_consumer?
- errors['Authorization oauth_token'] << 'does not belong to the specified consumer'
- end
- end
+ # nonce
+ if !nonce?
+ unless signature_method == 'PLAINTEXT'
+ errors['Authorization oauth_nonce'] << "is missing"
+ end
+ elsif nonce_used?
+ errors['Authorization oauth_nonce'] << "has already been used"
+ end
- # nonce
- if !nonce?
- errors['Authorization oauth_nonce'] << "is missing"
- elsif nonce_used?
- errors['Authorization oauth_nonce'] << "has already been used"
- end
+ # signature method
+ if !signature_method?
+ errors['Authorization oauth_signature_method'] << "is missing"
+ elsif !allowed_signature_methods.any? { |sm| signature_method.downcase == sm.downcase }
+ errors['Authorization oauth_signature_method'] << "must be one of " +
+ "#{allowed_signature_methods.join(', ')}; got: #{signature_method}"
+ end
- # signature method
- if !signature_method?
- errors['Authorization oauth_signature_method'] << "is missing"
- elsif !allowed_signature_methods.any? { |sm| signature_method.downcase == sm.downcase }
- errors['Authorization oauth_signature_method'] << "must be one of " +
- "#{allowed_signature_methods.join(', ')}; got: #{signature_method}"
- end
+ # signature
+ if !signature?
+ errors['Authorization oauth_signature'] << "is missing"
+ end
- # signature
- if !signature?
- errors['Authorization oauth_signature'] << "is missing"
- end
+ signable_request = SignableRequest.new(@attributes.merge(secrets).merge('authorization' => oauth_header_params))
- if errors.any?
- errors
- else
- # proceed to check signature
- if !simple_oauth_header.valid?(secrets)
- {'Authorization oauth_signature' => ['is invalid']}
+ # body hash
+
+ # present?
+ if body_hash?
+ # allowed?
+ if !signable_request.form_encoded?
+ # applicable?
+ if SignableRequest::BODY_HASH_METHODS.key?(signature_method)
+ # correct?
+ if body_hash == signable_request.body_hash
+ # all good
else
- use_nonce!
- nil
+ errors['Authorization oauth_body_hash'] << "is invalid"
end
+ else
+ # received a body hash with plaintext. weird situation - we will ignore it; signature will not
+ # be verified but it will be a part of the signature.
end
+ else
+ errors['Authorization oauth_body_hash'] << "must not be included with form-encoded requests"
end
+ else
+ # allowed?
+ if !signable_request.form_encoded?
+ # required?
+ if body_hash_required?
+ errors['Authorization oauth_body_hash'] << "is required (on non-form-encoded requests)"
+ else
+ # okay - not supported by client, but allowed
+ end
+ else
+ # all good
+ end
end
+
+ throw(:errors, errors) if errors.any?
+
+ # proceed to check signature
+ unless self.signature == signable_request.signature
+ throw(:errors, {'Authorization oauth_signature' => ['is invalid']})
+ end
+
+ use_nonce!
+ nil
end
end
require 'oauthenticator/config_methods'
include ConfigMethods
private
# hash of header params. keys should be a subset of OAUTH_ATTRIBUTE_KEYS.
def oauth_header_params
- @oauth_header_params ||= SimpleOAuth::Header.parse(authorization)
+ @oauth_header_params ||= OAuthenticator.parse_authorization(authorization)
end
- # reads the request body, be it String or IO
- def read_body
- if body.is_a?(String)
- body
- elsif body.respond_to?(:read) && body.respond_to?(:rewind)
- body.rewind
- body.read.tap do
- body.rewind
- end
- else
- raise NotImplementedError, "body = #{body.inspect}"
- end
- end
-
- # SimpleOAuth::Header for this request
- def simple_oauth_header
- params = media_type == "application/x-www-form-urlencoded" ? CGI.parse(read_body).map{|k,vs| vs.map{|v| [k,v] } }.inject([], &:+) : nil
- simple_oauth_header = SimpleOAuth::Header.new(request_method, url, params, authorization)
- end
-
# raise a nice error message for a method that needs to be implemented on a module of config methods
def config_method_not_implemented
caller_name = caller[0].match(%r(in `(.*?)'))[1]
- using_middleware = caller.any? { |l| l =~ %r(oauthenticator/middleware.rb:.*`call') }
+ using_middleware = caller.any? { |l| l =~ %r(oauthenticator/rack_authenticator.rb:.*`call') }
message = "method \##{caller_name} must be implemented on a module of oauth config methods, which is " + begin
if using_middleware
- "passed to OAuthenticator::Middleware using the option :config_methods."
+ "passed to OAuthenticator::RackAuthenticator using the option :config_methods."
else
"included in a subclass of OAuthenticator::SignedRequest, typically by passing it to OAuthenticator::SignedRequest.including_config(your_module)."
end
end + " Please consult the documentation."
raise NotImplementedError, message