Class: OAuthenticator::SignedRequest

Inherits:
Object
  • Object
show all
Includes:
ConfigMethods
Defined in:
lib/oauthenticator/signed_request.rb

Overview

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.

this class is not useful on its own, as various methods must be implemented on a module to be included before the implementation is complete enough to use. see the README and the documentation for the module ConfigMethods for details. to pass such a module to SignedRequest, use SignedRequest.including_config, like OAuthenticator::SignedRequest.including_config(config_module).

Constant Summary collapse

ATTRIBUTE_KEYS =

attributes of a SignedRequest

%w(request_method uri body media_type authorization).map(&:freeze).freeze
OAUTH_ATTRIBUTE_KEYS =

oauth attributes parsed from the request authorization

(SignableRequest::PROTOCOL_PARAM_KEYS + %w(signature body_hash)).freeze

Class Method Summary collapse

Instance Method Summary collapse

Methods included from ConfigMethods

#allowed_signature_methods, #body_hash_required?, #consumer_secret, #nonce_used?, #timestamp_valid_future, #timestamp_valid_past, #timestamp_valid_period, #token_belongs_to_consumer?, #token_secret, #use_nonce!

Constructor Details

#initialize(attributes) ⇒ SignedRequest

initialize a OAuthenticator::SignedRequest. this should not be called on OAuthenticator::SignedRequest directly, but a subclass made with including_config - see OAuthenticator::SignedRequest's documentation.



76
77
78
79
80
81
82
# File 'lib/oauthenticator/signed_request.rb', line 76

def initialize(attributes)
  @attributes = attributes.inject({}){|acc, (k,v)| acc.update((k.is_a?(Symbol) ? k.to_s : k) => v) }
  extra_attributes = @attributes.keys - ATTRIBUTE_KEYS
  if extra_attributes.any?
    raise ArgumentError, "received unrecognized attribute keys: #{extra_attributes.inspect}"
  end
end

Class Method Details

.from_rack_request(request) ⇒ subclass of OAuthenticator::SignedRequest

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

Parameters:

  • request (Rack::Request)

Returns:



63
64
65
66
67
68
69
70
71
# File 'lib/oauthenticator/signed_request.rb', line 63

def from_rack_request(request)
  new({
    :request_method => request.request_method,
    :uri => request.url,
    :body => request.body,
    :media_type => request.media_type,
    :authorization => request.env['HTTP_AUTHORIZATION'],
  })
end

.including_config(config_methods_module) ⇒ Class

returns a subclass of OAuthenticator::SignedRequest which includes the given config module

documentation for ConfigMethods and the README

Parameters:

  • config_methods_module (Module)

    a module which implements the methods described in the

Returns:

  • (Class)

    subclass of SignedRequest with the given module included



26
27
28
29
30
31
# File 'lib/oauthenticator/signed_request.rb', line 26

def including_config(config_methods_module)
  @extended_classes ||= Hash.new do |h, confmodule|
    h[confmodule] = Class.new(::OAuthenticator::SignedRequest).send(:include, confmodule)
  end
  @extended_classes[config_methods_module]
end

Instance Method Details

#errorsnil, Hash<String, Array<String>>

inspects the request represented by this instance of SignedRequest. if the request is authentically signed with OAuth, returns nil to indicate that there are no errors. if the request is inauthentic or invalid for any reason, this returns a hash containing the reason(s) why the request is invalid.

The error object's structure is a hash with string keys indicating attributes with errors, and values being arrays of strings indicating error messages on the attribute key. this structure takes after structured rails / ActiveResource, and looks like:

{'attribute1': ['messageA', 'messageB'], 'attribute2': ['messageC']}

Returns:

  • (nil, Hash<String, Array<String>>)

    either nil or a hash of errors



95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
# File 'lib/oauthenticator/signed_request.rb', line 95

def errors
  return @errors if instance_variables.any? { |ivar| ivar.to_s == '@errors' }
  @errors = catch(:errors) do
    if authorization.nil?
      throw(:errors, {'Authorization' => ["Authorization header is missing"]})
    elsif authorization !~ /\S/
      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'] << "Authorization oauth_timestamp is missing"
      end
    elsif timestamp !~ /\A\s*\d+\s*\z/
      errors['Authorization oauth_timestamp'] << "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'] << "Authorization oauth_timestamp is too old: #{timestamp}"
      elsif timestamp_i > Time.now.to_i + timestamp_valid_future
        errors['Authorization oauth_timestamp'] << "Authorization oauth_timestamp is too far in the future: #{timestamp}"
      end
    end

    # oauth version
    if version? && version != '1.0'
      errors['Authorization oauth_version'] << "Authorization oauth_version must be 1.0; got: #{version}"
    end

    # she's filled with secrets
    secrets = {}

    # consumer / client application
    if !consumer_key?
      errors['Authorization oauth_consumer_key'] << "Authorization oauth_consumer_key is missing"
    else
      secrets[:consumer_secret] = consumer_secret
      if !secrets[:consumer_secret]
        errors['Authorization oauth_consumer_key'] << 'Authorization oauth_consumer_key is invalid'
      end
    end

    # token
    if token?
      secrets[:token_secret] = token_secret
      if !secrets[:token_secret]
        errors['Authorization oauth_token'] << 'Authorization oauth_token is invalid'
      elsif !token_belongs_to_consumer?
        errors['Authorization oauth_token'] << 'Authorization oauth_token does not belong to the specified consumer'
      end
    end

    # nonce
    if !nonce?
      unless signature_method == 'PLAINTEXT'
        errors['Authorization oauth_nonce'] << "Authorization oauth_nonce is missing"
      end
    elsif nonce_used?
      errors['Authorization oauth_nonce'] << "Authorization oauth_nonce has already been used"
    end

    # signature method
    if !signature_method?
      errors['Authorization oauth_signature_method'] << "Authorization oauth_signature_method is missing"
    elsif !allowed_signature_methods.any? { |sm| signature_method.downcase == sm.downcase }
      errors['Authorization oauth_signature_method'] << "Authorization oauth_signature_method must be one of " +
        "#{allowed_signature_methods.join(', ')}; got: #{signature_method}"
    end

    # signature
    if !signature?
      errors['Authorization oauth_signature'] << "Authorization oauth_signature is missing"
    end

    signable_request = SignableRequest.new(@attributes.merge(secrets).merge('authorization' => oauth_header_params))

    # 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
            errors['Authorization oauth_body_hash'] << "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'] << "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'] << "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' => ['Authorization oauth_signature is invalid']})
    end

    if nonce?
      begin
        use_nonce!
      rescue NonceUsedError
        throw(:errors, {'Authorization oauth_nonce' => ['Authorization oauth_nonce has already been used']})
      end
    end

    nil
  end
end