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