Module: Signet::OAuth1

Defined in:
lib/signet/oauth_1/credential.rb,
lib/signet/oauth_1.rb,
lib/signet/oauth_1/client.rb,
lib/signet/oauth_1/server.rb,
lib/signet/oauth_1/signature_methods/rsa_sha1.rb,
lib/signet/oauth_1/signature_methods/hmac_sha1.rb,
lib/signet/oauth_1/signature_methods/plaintext.rb

Overview

:nodoc:

Defined Under Namespace

Modules: HMACSHA1, PLAINTEXT, RSASHA1 Classes: Client, Credential, Server

Constant Summary collapse

OUT_OF_BAND =
"oob".freeze

Class Method Summary collapse

Class Method Details

.encode(value) ⇒ String

Converts a value to a percent-encoded String according to the rules given in RFC 5849. All non-unreserved characters are percent-encoded.

Parameters:

  • value (Symbol, #to_str)

    The value to be encoded.

Returns:

  • (String)

    The percent-encoded value.


18
19
20
21
22
23
24
# File 'lib/signet/oauth_1.rb', line 18

def self.encode value
  value = value.to_s if value.is_a? Symbol
  Addressable::URI.encode_component(
    value,
    Addressable::URI::CharacterClasses::UNRESERVED
  )
end

.extract_credential_key_option(credential_type, options) ⇒ String

Processes an options Hash to find a credential key value. Allows for greater flexibility in configuration.

Parameters:

  • credential_type (Symbol)

    One of :client, :temporary, :token, :consumer, :request, or :access.

Returns:

  • (String)

    The credential key value.


65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/signet/oauth_1.rb', line 65

def self.extract_credential_key_option credential_type, options
  # Normalize key to String to allow indifferent access.
  options = options.to_h.transform_keys(&:to_s)
  credential_key = "#{credential_type}_credential_key"
  credential = "#{credential_type}_credential"
  if options[credential_key]
    credential_key = options[credential_key]
  elsif options[credential]
    require "signet/oauth_1/credential"
    unless options[credential].respond_to? :key
      raise TypeError,
            "Expected Signet::OAuth1::Credential, " \
            "got #{options[credential].class}."
    end
    credential_key = options[credential].key
  elsif options["client"]
    require "signet/oauth_1/client"
    unless options["client"].is_a? ::Signet::OAuth1::Client
      raise TypeError,
            "Expected Signet::OAuth1::Client, got #{options['client'].class}."
    end
    credential_key = options["client"].send credential_key
  else
    credential_key = nil
  end
  if !credential_key.nil? && !credential_key.is_a?(String)
    raise TypeError,
          "Expected String, got #{credential_key.class}."
  end
  credential_key
end

.extract_credential_secret_option(credential_type, options) ⇒ String

Processes an options Hash to find a credential secret value. Allows for greater flexibility in configuration.

Parameters:

  • credential_type (Symbol)

    One of :client, :temporary, :token, :consumer, :request, or :access.

Returns:

  • (String)

    The credential secret value.


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
# File 'lib/signet/oauth_1.rb', line 107

def self.extract_credential_secret_option credential_type, options
  # Normalize key to String to allow indifferent access.
  options = options.to_h.transform_keys(&:to_s)
  credential_secret = "#{credential_type}_credential_secret"
  credential = "#{credential_type}_credential"
  if options[credential_secret]
    credential_secret = options[credential_secret]
  elsif options[credential]
    require "signet/oauth_1/credential"
    unless options[credential].respond_to? :secret
      raise TypeError,
            "Expected Signet::OAuth1::Credential, " \
            "got #{options[credential].class}."
    end
    credential_secret = options[credential].secret
  elsif options["client"]
    require "signet/oauth_1/client"
    unless options["client"].is_a? ::Signet::OAuth1::Client
      raise TypeError,
            "Expected Signet::OAuth1::Client, got #{options['client'].class}."
    end
    credential_secret = options["client"].send credential_secret
  else
    credential_secret = nil
  end
  if !credential_secret.nil? && !credential_secret.is_a?(String)
    raise TypeError,
          "Expected String, got #{credential_secret.class}."
  end
  credential_secret
end

.generate_authorization_header(parameters, realm = nil) ⇒ String

Generates an Authorization header from a parameter list according to the rules given in RFC 5849.

Parameters:

  • parameters (Enumerable)

    The OAuth parameter list.

  • realm (String) (defaults to: nil)

    The Authorization realm. See RFC 2617.

Returns:

  • (String)

    The Authorization header.


200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
# File 'lib/signet/oauth_1.rb', line 200

def self.generate_authorization_header parameters, realm = nil
  if !parameters.is_a?(Enumerable) || parameters.is_a?(String)
    raise TypeError, "Expected Enumerable, got #{parameters.class}."
  end
  parameter_list = parameters.map do |k, v|
    if k == "realm"
      raise ArgumentError,
            'The "realm" parameter must be specified as a separate argument.'
    end
    "#{encode k}=\"#{encode v}\""
  end
  if realm
    realm = realm.gsub '"', '\"'
    parameter_list.unshift "realm=\"#{realm}\""
  end
  "OAuth #{parameter_list.join ', '}"
end

.generate_authorization_uri(authorization_uri, options = {}) ⇒ String

Appends the optional 'oauth_token' and 'oauth_callback' parameters to the base authorization URI.

Parameters:

  • authorization_uri (Addressable::URI, String, #to_str)

    The base authorization URI.

Returns:

  • (String)

    The authorization URI to redirect the user to.


342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
# File 'lib/signet/oauth_1.rb', line 342

def self.generate_authorization_uri authorization_uri, options = {}
  options = {
    callback:              nil,
    additional_parameters: {}
  }.merge(options)
  temporary_credential_key =
    extract_credential_key_option :temporary, options
  parsed_uri = Addressable::URI.parse(authorization_uri).dup
  query_values = parsed_uri.query_values || {}
  if options[:additional_parameters]
    query_values = query_values.merge(
      options[:additional_parameters].each_with_object({}) { |(k, v), h| h[k] = v; }
    )
  end
  query_values["oauth_token"] = temporary_credential_key if temporary_credential_key
  query_values["oauth_callback"] = options[:callback] if options[:callback]
  parsed_uri.query_values = query_values
  parsed_uri.normalize.to_s
end

.generate_base_string(method, uri, parameters) ⇒ String

Generates a signature base string according to the algorithm given in RFC 5849. Joins the method, URI, and normalized parameter string with '&' characters.

Parameters:

  • method (String)

    The HTTP method.

  • uri (Addressable::URI, String, #to_str)

    The URI.

  • parameters (Enumerable)

    The OAuth parameter list.

Returns:

  • (String)

    The signature base string.

Raises:

  • (TypeError)

168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/signet/oauth_1.rb', line 168

def self.generate_base_string method, uri, parameters
  raise TypeError, "Expected Enumerable, got #{parameters.class}." unless parameters.is_a? Enumerable
  method = method.to_s.upcase
  parsed_uri = Addressable::URI.parse uri
  uri = Addressable::URI.new(
    scheme:    parsed_uri.normalized_scheme,
    authority: parsed_uri.normalized_authority,
    path:      parsed_uri.path,
    query:     parsed_uri.query,
    fragment:  parsed_uri.fragment
  )
  uri_parameters = uri.query_values(Array) || []
  uri = uri.omit(:query, :fragment).to_s
  merged_parameters =
    uri_parameters.concat(parameters.map { |k, v| [k, v] })
  parameter_string = normalize_parameters merged_parameters
  [
    encode(method),
    encode(uri),
    encode(parameter_string)
  ].join("&")
end

.generate_nonceString

Returns a nonce suitable for use as an 'oauth_nonce' value.

Returns:

  • (String)

    A random nonce.


51
52
53
# File 'lib/signet/oauth_1.rb', line 51

def self.generate_nonce
  SecureRandom.random_bytes(16).unpack("H*").join
end

.generate_timestampString

Returns a timestamp suitable for use as an 'oauth_timestamp' value.

Returns:

  • (String)

    The current timestamp.


42
43
44
# File 'lib/signet/oauth_1.rb', line 42

def self.generate_timestamp
  Time.now.to_i.to_s
end

.normalize_parameters(parameters) ⇒ String

Normalizes a set of OAuth parameters according to the algorithm given in RFC 5849. Sorts key/value pairs lexically by byte order, first by key, then by value, joins key/value pairs with the '=' character, then joins the entire parameter list with '&' characters.

Parameters:

  • parameters (Enumerable)

    The OAuth parameter list.

Returns:

  • (String)

    The normalized parameter list.

Raises:

  • (TypeError)

148
149
150
151
152
153
154
155
156
# File 'lib/signet/oauth_1.rb', line 148

def self.normalize_parameters parameters
  raise TypeError, "Expected Enumerable, got #{parameters.class}." unless parameters.is_a? Enumerable
  parameter_list = parameters.map do |k, v|
    next if k == "oauth_signature"
    # This is probably the wrong place to try to exclude the realm
    "#{encode k}=#{encode v}"
  end
  parameter_list.compact.sort.join "&"
end

.parse_authorization_header(field_value) ⇒ Object

Parses an Authorization header into its component parameters. Parameter keys and values are decoded according to the rules given in RFC 5849.

Raises:

  • (TypeError)

222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# File 'lib/signet/oauth_1.rb', line 222

def self.parse_authorization_header field_value
  raise TypeError, "Expected String, got #{field_value.class}." unless field_value.is_a? String
  auth_scheme = field_value[/^([-._0-9a-zA-Z]+)/, 1]
  case auth_scheme
  when /^OAuth$/i
    # Other token types may be supported eventually
    pairs = Signet.parse_auth_param_list(field_value[/^OAuth\s+(.*)$/i, 1])
    (pairs.each_with_object [] do |(k, v), accu|
      if k != "realm"
        k = unencode k
        v = unencode v
      end
      accu << [k, v]
    end)
  else
    raise ParseError,
          "Parsing non-OAuth Authorization headers is out of scope."
  end
end

.parse_form_encoded_credentials(body) ⇒ Signet::OAuth1::Credential

Parses an application/x-www-form-urlencoded HTTP response body into an OAuth key/secret pair.

Parameters:

  • body (String)

    The response body.

Returns:

Raises:

  • (TypeError)

249
250
251
252
253
254
# File 'lib/signet/oauth_1.rb', line 249

def self.parse_form_encoded_credentials body
  raise TypeError, "Expected String, got #{body.class}." unless body.is_a? String
  Signet::OAuth1::Credential.new(
    Addressable::URI.form_unencode(body)
  )
end

.sign_parameters(method, uri, parameters, client_credential_secret, token_credential_secret = nil) ⇒ String

Generates an OAuth signature using the signature method indicated in the parameter list. Unsupported signature methods will result in a NotImplementedError exception being raised.

Parameters:

  • method (String)

    The HTTP method.

  • uri (Addressable::URI, String, #to_str)

    The URI.

  • parameters (Enumerable)

    The OAuth parameter list.

  • client_credential_secret (String)

    The client credential secret.

  • token_credential_secret (String) (defaults to: nil)

    The token credential secret. Omitted when unavailable.

Returns:

  • (String)

    The signature.


269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
# File 'lib/signet/oauth_1.rb', line 269

def self.sign_parameters method, uri, parameters,
                         client_credential_secret, token_credential_secret = nil
  # Technically, the token_credential_secret parameter here may actually
  # be a temporary credential secret when obtaining a token credential
  # for the first time
  base_string = generate_base_string method, uri, parameters
  parameters = parameters.to_h.transform_keys(&:to_s)
  signature_method = parameters["oauth_signature_method"]
  case signature_method
  when "HMAC-SHA1"
    require "signet/oauth_1/signature_methods/hmac_sha1"
    Signet::OAuth1::HMACSHA1.generate_signature base_string, client_credential_secret, token_credential_secret
  when "RSA-SHA1"
    require "signet/oauth_1/signature_methods/rsa_sha1"
    Signet::OAuth1::RSASHA1.generate_signature base_string, client_credential_secret, token_credential_secret
  when "PLAINTEXT"
    require "signet/oauth_1/signature_methods/plaintext"
    Signet::OAuth1::PLAINTEXT.generate_signature base_string, client_credential_secret, token_credential_secret
  else
    raise NotImplementedError,
          "Unsupported signature method: #{signature_method}"
  end
end

.unencode(value) ⇒ String

Converts a percent-encoded String to an unencoded value.

Parameters:

  • value (#to_str)

    The percent-encoded String to be unencoded.

Returns:

  • (String)

    The unencoded value.


33
34
35
# File 'lib/signet/oauth_1.rb', line 33

def self.unencode value
  Addressable::URI.unencode_component value
end

.unsigned_resource_parameters(options = {}) ⇒ Array

Generates an OAuth parameter list to be used when requesting a protected resource.

Parameters:

  • options (Hash) (defaults to: {})

    The configuration parameters for the request.

    • :client_credential_key - The client credential key.
    • :token_credential_key - The token credential key.
    • :signature_method - The signature method. Defaults to 'HMAC-SHA1'.
    • :two_legged - A switch for two-legged OAuth. Defaults to false.

Returns:

  • (Array)

    The parameter list as an Array of key/value pairs.

Raises:

  • (ArgumentError)

419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
# File 'lib/signet/oauth_1.rb', line 419

def self.unsigned_resource_parameters options = {}
  options = {
    signature_method: "HMAC-SHA1",
    two_legged:       false
  }.merge(options)
  client_credential_key =
    extract_credential_key_option :client, options
  raise ArgumentError, "Missing :client_credential_key parameter." if client_credential_key.nil?
  unless options[:two_legged]
    token_credential_key =
      extract_credential_key_option :token, options
    raise ArgumentError, "Missing :token_credential_key parameter." if token_credential_key.nil?
  end
  parameters = [
    ["oauth_consumer_key", client_credential_key],
    ["oauth_signature_method", options[:signature_method]],
    ["oauth_timestamp", generate_timestamp],
    ["oauth_nonce", generate_nonce],
    ["oauth_version", "1.0"]
  ]
  parameters << ["oauth_token", token_credential_key] unless options[:two_legged]
  # No additional parameters allowed here
  parameters
end

.unsigned_temporary_credential_parameters(options = {}) ⇒ Array

Generates an OAuth parameter list to be used when obtaining a set of temporary credentials.

Parameters:

  • options (Hash) (defaults to: {})

    The configuration parameters for the request.

    • :client_credential_key - The client credential key.
    • :callback - The OAuth callback. Defaults to OUT_OF_BAND.
    • :signature_method - The signature method. Defaults to 'HMAC-SHA1'.
    • :additional_parameters - Non-standard additional parameters.

Returns:

  • (Array)

    The parameter list as an Array of key/value pairs.

Raises:

  • (ArgumentError)

310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
# File 'lib/signet/oauth_1.rb', line 310

def self.unsigned_temporary_credential_parameters options = {}
  options = {
    callback:              ::Signet::OAuth1::OUT_OF_BAND,
    signature_method:      "HMAC-SHA1",
    additional_parameters: []
  }.merge(options)
  client_credential_key =
    extract_credential_key_option :client, options
  raise ArgumentError, "Missing :client_credential_key parameter." if client_credential_key.nil?
  parameters = [
    ["oauth_consumer_key", client_credential_key],
    ["oauth_signature_method", options[:signature_method]],
    ["oauth_timestamp", generate_timestamp],
    ["oauth_nonce", generate_nonce],
    ["oauth_version", "1.0"],
    ["oauth_callback", options[:callback]]
  ]
  # Works for any Enumerable
  options[:additional_parameters].each do |key, value|
    parameters << [key, value]
  end
  parameters
end

.unsigned_token_credential_parameters(options = {}) ⇒ Array

Generates an OAuth parameter list to be used when obtaining a set of token credentials.

Parameters:

  • options (Hash) (defaults to: {})

    The configuration parameters for the request.

    • :client_credential_key - The client credential key.
    • :temporary_credential_key - The temporary credential key.
    • :verifier - The OAuth verifier.
    • :signature_method - The signature method. Defaults to 'HMAC-SHA1'.

Returns:

  • (Array)

    The parameter list as an Array of key/value pairs.

Raises:

  • (ArgumentError)

379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
# File 'lib/signet/oauth_1.rb', line 379

def self.unsigned_token_credential_parameters options = {}
  options = {
    signature_method: "HMAC-SHA1",
    verifier:         nil
  }.merge(options)
  client_credential_key =
    extract_credential_key_option :client, options
  temporary_credential_key =
    extract_credential_key_option :temporary, options
  raise ArgumentError, "Missing :client_credential_key parameter." if client_credential_key.nil?
  raise ArgumentError, "Missing :temporary_credential_key parameter." if temporary_credential_key.nil?
  raise ArgumentError, "Missing :verifier parameter." if options[:verifier].nil?
  [
    ["oauth_consumer_key", client_credential_key],
    ["oauth_token", temporary_credential_key],
    ["oauth_signature_method", options[:signature_method]],
    ["oauth_timestamp", generate_timestamp],
    ["oauth_nonce", generate_nonce],
    ["oauth_verifier", options[:verifier]],
    ["oauth_version", "1.0"]
  ]
end