simple_oauth 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,133 +1,274 @@
1
- require 'openssl'
2
- require 'uri'
3
- require 'base64'
4
- require 'cgi'
1
+ require "cgi"
2
+ require "uri"
3
+ require_relative "encoding"
4
+ require_relative "errors"
5
+ require_relative "parser"
6
+ require_relative "signature"
7
+ require_relative "header/class_methods"
5
8
 
6
9
  module SimpleOAuth
10
+ # Generates OAuth 1.0 Authorization headers for HTTP requests
11
+ #
12
+ # @api public
7
13
  class Header
8
- ATTRIBUTE_KEYS = [:callback, :consumer_key, :nonce, :signature_method, :timestamp, :token, :verifier, :version] unless defined? ::SimpleOAuth::Header::ATTRIBUTE_KEYS
14
+ # OAuth header scheme prefix
15
+ OAUTH_SCHEME = "OAuth".freeze
9
16
 
10
- IGNORED_KEYS = [:consumer_secret, :token_secret, :signature] unless defined? ::SimpleOAuth::Header::IGNORED_KEYS
17
+ # Prefix for OAuth parameters
18
+ OAUTH_PREFIX = "oauth_".freeze
11
19
 
12
- attr_reader :method, :params, :options
20
+ # Default signature method per RFC 5849
21
+ DEFAULT_SIGNATURE_METHOD = "HMAC-SHA1".freeze
13
22
 
14
- class << self
15
- def default_options
16
- {
17
- :nonce => OpenSSL::Random.random_bytes(16).unpack('H*')[0],
18
- :signature_method => 'HMAC-SHA1',
19
- :timestamp => Time.now.to_i.to_s,
20
- :version => '1.0',
21
- }
22
- end
23
+ # OAuth version
24
+ OAUTH_VERSION = "1.0".freeze
23
25
 
24
- def parse(header)
25
- header.to_s.sub(/^OAuth\s/, '').split(/,\s*/).inject({}) do |attributes, pair|
26
- match = pair.match(/^(\w+)\=\"([^\"]*)\"$/)
27
- attributes.merge(match[1].sub(/^oauth_/, '').to_sym => unescape(match[2]))
28
- end
29
- end
26
+ # Valid OAuth attribute keys that can be included in the header
27
+ ATTRIBUTE_KEYS = %i[body_hash callback consumer_key nonce signature_method timestamp token verifier version].freeze
30
28
 
31
- def escape(value)
32
- uri_parser.escape(value.to_s, /[^a-z0-9\-\.\_\~]/i)
33
- end
34
- alias_method :encode, :escape
29
+ # Keys that are used internally but should not appear in attributes
30
+ IGNORED_KEYS = %i[consumer_secret token_secret signature realm ignore_extra_keys].freeze
35
31
 
36
- def unescape(value)
37
- uri_parser.unescape(value.to_s)
38
- end
39
- alias_method :decode, :unescape
32
+ # Valid keys when parsing OAuth parameters (ATTRIBUTE_KEYS + signature)
33
+ PARSE_KEYS = [*ATTRIBUTE_KEYS, :signature].freeze
40
34
 
41
- private
35
+ # The HTTP method for the request
36
+ #
37
+ # @return [String] the HTTP method (GET, POST, etc.)
38
+ # @example
39
+ # header.method # => "GET"
40
+ attr_reader :method
42
41
 
43
- def uri_parser
44
- @uri_parser ||= URI.const_defined?(:Parser) ? URI::Parser.new : URI
45
- end
46
- end
42
+ # The request parameters to be signed
43
+ #
44
+ # @return [Hash] the request parameters
45
+ # @example
46
+ # header.params # => {"status" => "Hello"}
47
+ attr_reader :params
48
+
49
+ # The raw request body for oauth_body_hash computation
50
+ #
51
+ # @return [String, nil] the request body
52
+ # @example
53
+ # header.body # => '{"text": "Hello"}'
54
+ attr_reader :body
55
+
56
+ # The OAuth options including credentials and signature
57
+ #
58
+ # @return [Hash] the OAuth options
59
+ # @example
60
+ # header.options # => {consumer_key: "key", nonce: "..."}
61
+ attr_reader :options
47
62
 
48
- def initialize(method, url, params, oauth = {})
63
+ extend ClassMethods
64
+ extend Encoding
65
+
66
+ # Creates a new OAuth header
67
+ #
68
+ # @api public
69
+ # @param method [String, Symbol] the HTTP method
70
+ # @param url [String, URI] the request URL
71
+ # @param params [Hash] the request parameters (for form-encoded bodies)
72
+ # @param oauth [Hash, String] OAuth options hash or an existing Authorization header to parse
73
+ # @param body [String, nil] raw request body for oauth_body_hash (for non-form-encoded bodies)
74
+ # @example Create a header with OAuth options
75
+ # SimpleOAuth::Header.new(:get, "https://api.example.com/resource", {},
76
+ # consumer_key: "key", consumer_secret: "secret")
77
+ # @example Create a header by parsing an existing Authorization header
78
+ # SimpleOAuth::Header.new(:get, "https://api.example.com/resource", {}, existing_header)
79
+ # @example Create a header with a JSON body (oauth_body_hash will be computed)
80
+ # SimpleOAuth::Header.new(:post, "https://api.example.com/resource", {},
81
+ # {consumer_key: "key", consumer_secret: "secret"}, '{"text": "Hello"}')
82
+ def initialize(method, url, params, oauth = {}, body = nil)
49
83
  @method = method.to_s.upcase
50
- @uri = URI.parse(url.to_s)
51
- @uri.scheme = @uri.scheme.downcase
52
- @uri.normalize!
53
- @uri.fragment = nil
84
+ @uri = normalize_uri(url)
54
85
  @params = params
55
- @options = oauth.is_a?(Hash) ? self.class.default_options.merge(oauth) : self.class.parse(oauth)
86
+ @body = body
87
+ @options = build_options(oauth, body)
56
88
  end
57
89
 
90
+ # Returns the normalized URL without query string or fragment
91
+ #
92
+ # @api public
93
+ # @return [String] the normalized URL
94
+ # @example
95
+ # header = SimpleOAuth::Header.new(:get, "https://api.example.com/path?query=1", {})
96
+ # header.url
97
+ # # => "https://api.example.com/path"
58
98
  def url
59
- uri = @uri.dup
60
- uri.query = nil
61
- uri.to_s
99
+ @uri.dup.tap { |uri| uri.query = nil }.to_str
62
100
  end
63
101
 
102
+ # Returns the OAuth Authorization header string
103
+ #
104
+ # @api public
105
+ # @return [String] the Authorization header value
106
+ # @example
107
+ # header = SimpleOAuth::Header.new(:get, "https://api.example.com/", {},
108
+ # consumer_key: "key", consumer_secret: "secret")
109
+ # header.to_s
110
+ # # => "OAuth oauth_consumer_key=\"key\", oauth_nonce=\"...\", ..."
64
111
  def to_s
65
- "OAuth #{normalized_attributes}"
112
+ "#{OAUTH_SCHEME} #{normalized_attributes}"
66
113
  end
67
114
 
115
+ # Validates the signature in the header against the provided secrets
116
+ #
117
+ # @api public
118
+ # @param secrets [Hash] the consumer_secret and token_secret for validation
119
+ # @return [Boolean] true if the signature is valid, false otherwise
120
+ # @example
121
+ # parsed_header = SimpleOAuth::Header.new(:get, url, {}, authorization_header)
122
+ # parsed_header.valid?(consumer_secret: "secret", token_secret: "token_secret")
123
+ # # => true
68
124
  def valid?(secrets = {})
69
- original_options = options.dup
125
+ original_options = options.dup #: Hash[Symbol, untyped]
70
126
  options.merge!(secrets)
71
- valid = options[:signature] == signature
127
+ options.fetch(:signature).eql?(signature)
128
+ ensure
72
129
  options.replace(original_options)
73
- valid
74
130
  end
75
131
 
132
+ # Returns the OAuth attributes including the signature
133
+ #
134
+ # @api public
135
+ # @return [Hash] OAuth attributes with oauth_signature included
136
+ # @example
137
+ # header.signed_attributes
138
+ # # => {oauth_consumer_key: "key", oauth_signature: "...", ...}
76
139
  def signed_attributes
77
- attributes.merge(:oauth_signature => signature)
140
+ header_attributes.merge(oauth_signature: signature)
78
141
  end
79
142
 
80
- private
143
+ private
81
144
 
82
- def normalized_attributes
83
- signed_attributes.sort_by { |k, _| k.to_s }.collect { |k, v| %(#{k}="#{self.class.escape(v)}") }.join(', ')
145
+ # Normalizes and parses a URL into a URI object
146
+ #
147
+ # @api private
148
+ # @param url [String, URI] the URL to normalize
149
+ # @return [URI::Generic] normalized URI without fragment
150
+ def normalize_uri(url)
151
+ URI.parse(url.to_s).tap do |uri|
152
+ uri.normalize!
153
+ uri.fragment = nil
154
+ end
84
155
  end
85
156
 
86
- def attributes
87
- matching_keys, extra_keys = options.keys.partition { |key| ATTRIBUTE_KEYS.include?(key) }
88
- extra_keys -= IGNORED_KEYS
89
- if options[:ignore_extra_keys] || extra_keys.empty?
90
- Hash[options.select { |key, _| matching_keys.include?(key) }.collect { |key, value| [:"oauth_#{key}", value] }]
157
+ # Builds OAuth options from input (hash or header string)
158
+ #
159
+ # @api private
160
+ # @param oauth [Hash, String] OAuth options hash or Authorization header
161
+ # @param body [String, nil] request body for body_hash computation
162
+ # @return [Hash] merged OAuth options with defaults
163
+ def build_options(oauth, body)
164
+ if oauth.is_a?(Hash)
165
+ self.class.default_options(body).merge(oauth.transform_keys(&:to_sym))
91
166
  else
92
- fail "SimpleOAuth: Found extra option keys not matching ATTRIBUTE_KEYS:\n [#{extra_keys.collect(&:inspect).join(', ')}]"
167
+ self.class.parse(oauth)
93
168
  end
94
169
  end
95
170
 
96
- def signature
97
- send(options[:signature_method].downcase.tr('-', '_') + '_signature')
171
+ # Builds the normalized OAuth attributes string for the header
172
+ #
173
+ # @api private
174
+ # @return [String] normalized OAuth attributes for the header
175
+ def normalized_attributes
176
+ signed_attributes
177
+ .sort_by { |key, _| key }
178
+ .map { |key, value| "#{key}=\"#{Header.escape(value)}\"" }
179
+ .join(", ")
98
180
  end
99
181
 
100
- def hmac_sha1_signature
101
- Base64.encode64(OpenSSL::HMAC.digest(OpenSSL::Digest::SHA1.new, secret, signature_base)).chomp.gsub(/\n/, '')
182
+ # Extracts valid OAuth attributes from options
183
+ #
184
+ # @api private
185
+ # @return [Hash] OAuth attributes without signature or realm
186
+ def attributes
187
+ validate_option_keys!
188
+ options.slice(*ATTRIBUTE_KEYS).transform_keys { |key| :"#{OAUTH_PREFIX}#{key}" }
102
189
  end
103
190
 
104
- def secret
105
- options.values_at(:consumer_secret, :token_secret).collect { |v| self.class.escape(v) }.join('&')
191
+ # Validates that no unknown keys are present in options
192
+ #
193
+ # @api private
194
+ # @raise [InvalidOptionsError] if extra keys are found
195
+ # @return [void]
196
+ def validate_option_keys!
197
+ return if options[:ignore_extra_keys]
198
+
199
+ extra_keys = options.keys - ATTRIBUTE_KEYS - IGNORED_KEYS
200
+ return if extra_keys.empty?
201
+
202
+ raise InvalidOptionsError, "Unknown option keys: #{extra_keys.map(&:inspect).join(", ")}"
106
203
  end
107
- alias_method :plaintext_signature, :secret
108
204
 
109
- def signature_base
110
- [method, url, normalized_params].collect { |v| self.class.escape(v) }.join('&')
205
+ # Returns OAuth attributes with realm for the Authorization header
206
+ #
207
+ # Per RFC 5849 Section 3.5.1, realm is included in the Authorization header
208
+ # but excluded from signature calculation (Section 3.4.1.3.1)
209
+ #
210
+ # @api private
211
+ # @return [Hash] OAuth attributes with realm if present
212
+ def header_attributes
213
+ attrs = attributes
214
+ attrs[:realm] = options.fetch(:realm) if options[:realm]
215
+ attrs
111
216
  end
112
217
 
113
- def normalized_params
114
- signature_params.collect { |p| p.collect { |v| self.class.escape(v) } }.sort.collect { |p| p.join('=') }.join('&')
218
+ # Extracts query parameters from the request URL
219
+ #
220
+ # @api private
221
+ # @return [Array<Array>] URL query parameters as key-value pairs
222
+ def url_params
223
+ CGI.parse(@uri.query || "").flat_map do |key, values|
224
+ values.sort.map { |value| [key, value] }
225
+ end
115
226
  end
116
227
 
117
- def signature_params
118
- attributes.to_a + params.to_a + url_params
228
+ # Computes the OAuth signature using the configured method
229
+ #
230
+ # @api private
231
+ # @return [String] the computed signature based on signature_method
232
+ def signature
233
+ sig_method = options.fetch(:signature_method)
234
+ sig_secret = Signature.rsa?(sig_method) ? options[:consumer_secret] : secret
235
+ Signature.sign(sig_method, sig_secret, signature_base)
119
236
  end
120
237
 
121
- def url_params
122
- CGI.parse(@uri.query || '').inject([]) { |p, (k, vs)| p + vs.sort.collect { |v| [k, v] } }
238
+ # Builds the secret string from consumer and token secrets
239
+ #
240
+ # @api private
241
+ # @return [String] the secret string for signing
242
+ def secret
243
+ options.values_at(:consumer_secret, :token_secret).map { |v| Header.escape(v) }.join("&")
244
+ end
245
+
246
+ # Builds the signature base string from method, URL, and params
247
+ #
248
+ # @api private
249
+ # @return [String] the signature base string
250
+ def signature_base
251
+ [method, url, normalized_params].map { |v| Header.escape(v) }.join("&")
123
252
  end
124
253
 
125
- def rsa_sha1_signature
126
- Base64.encode64(private_key.sign(OpenSSL::Digest::SHA1.new, signature_base)).chomp.gsub(/\n/, '')
254
+ # Normalizes and sorts all request parameters for signing
255
+ #
256
+ # @api private
257
+ # @return [String] normalized request parameters
258
+ def normalized_params
259
+ signature_params
260
+ .map { |key, value| [Header.escape(key), Header.escape(value)] }
261
+ .sort
262
+ .map { |pair| pair.join("=") }
263
+ .join("&")
127
264
  end
128
265
 
129
- def private_key
130
- OpenSSL::PKey::RSA.new(options[:consumer_secret])
266
+ # Collects all parameters to include in signature
267
+ #
268
+ # @api private
269
+ # @return [Array<Array>] all parameters for signature as key-value pairs
270
+ def signature_params
271
+ attributes.to_a + params.to_a + url_params
131
272
  end
132
273
  end
133
274
  end
@@ -0,0 +1,107 @@
1
+ require "strscan"
2
+ require_relative "errors"
3
+
4
+ module SimpleOAuth
5
+ # Parses OAuth Authorization headers
6
+ #
7
+ # @api private
8
+ class Parser
9
+ # Pattern to match OAuth key-value pairs: key="value"
10
+ PARAM_PATTERN = /(\w+)="([^"]*)"\s*(,?)\s*/
11
+
12
+ # OAuth scheme prefix
13
+ OAUTH_PREFIX = /OAuth\s+/
14
+
15
+ # The StringScanner instance for parsing the header
16
+ #
17
+ # @return [StringScanner] the scanner
18
+ attr_reader :scanner
19
+
20
+ # The parsed OAuth attributes
21
+ #
22
+ # @return [Hash{Symbol => String}] the parsed attributes
23
+ attr_reader :attributes
24
+
25
+ # Creates a new Parser for the given header string
26
+ #
27
+ # @param header [String, #to_s] the OAuth Authorization header string
28
+ # @return [Parser] a new parser instance
29
+ def initialize(header)
30
+ @scanner = StringScanner.new(header.to_s)
31
+ @attributes = {}
32
+ end
33
+
34
+ # Parses the OAuth Authorization header
35
+ #
36
+ # @param valid_keys [Array<Symbol>] the valid OAuth parameter keys
37
+ # @return [Hash{Symbol => String}] the parsed attributes
38
+ # @raise [SimpleOAuth::ParseError] if the header is malformed
39
+ def parse(valid_keys)
40
+ scan_oauth_prefix
41
+ scan_params(valid_keys)
42
+ verify_complete
43
+ attributes
44
+ end
45
+
46
+ private
47
+
48
+ # Scans and validates the OAuth prefix
49
+ #
50
+ # @return [void]
51
+ # @raise [SimpleOAuth::ParseError] if the header doesn't start with "OAuth "
52
+ def scan_oauth_prefix
53
+ return if scanner.scan(OAUTH_PREFIX)
54
+
55
+ raise ParseError, "Authorization header must start with 'OAuth '"
56
+ end
57
+
58
+ # Scans all key-value parameters from the header
59
+ #
60
+ # @param valid_keys [Array<Symbol>] the valid OAuth parameter keys
61
+ # @return [void]
62
+ def scan_params(valid_keys)
63
+ while scanner.scan(PARAM_PATTERN)
64
+ key = scanner[1] #: String
65
+ value = scanner[2] #: String
66
+ comma = scanner[3] #: String
67
+ validate_comma_separator(key, comma)
68
+ store_if_valid(key, value, valid_keys)
69
+ end
70
+ end
71
+
72
+ # Validates that a comma separator exists between parameters
73
+ #
74
+ # @param key [String] the parameter key for error messages
75
+ # @param comma [String] the comma separator (empty string if missing)
76
+ # @return [void]
77
+ # @raise [SimpleOAuth::ParseError] if comma is missing and more content follows
78
+ def validate_comma_separator(key, comma)
79
+ return if !comma.empty? || scanner.eos?
80
+
81
+ raise ParseError,
82
+ "Expected comma after '#{key}' parameter at position #{scanner.pos}: #{scanner.rest.inspect}"
83
+ end
84
+
85
+ # Stores the parameter if it's a valid OAuth key
86
+ #
87
+ # @param key [String] the raw parameter key (e.g., "oauth_consumer_key")
88
+ # @param value [String] the parameter value
89
+ # @param valid_keys [Array<Symbol>] the valid OAuth parameter keys
90
+ # @return [void]
91
+ def store_if_valid(key, value, valid_keys)
92
+ parsed_key = valid_keys.find { |k| "oauth_#{k}".eql?(key) }
93
+ attributes[parsed_key] = Header.unescape(value) if parsed_key
94
+ end
95
+
96
+ # Verifies that the entire header was parsed
97
+ #
98
+ # @return [void]
99
+ # @raise [SimpleOAuth::ParseError] if unparsed content remains
100
+ def verify_complete
101
+ return if scanner.eos?
102
+
103
+ raise ParseError,
104
+ "Could not parse parameter at position #{scanner.pos}: #{scanner.rest.inspect}"
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,191 @@
1
+ require "base64"
2
+ require "openssl"
3
+
4
+ module SimpleOAuth
5
+ # Signature computation methods for OAuth 1.0
6
+ #
7
+ # This module provides a registry of signature methods that can be extended
8
+ # with custom implementations. Built-in methods include HMAC-SHA1, HMAC-SHA256,
9
+ # RSA-SHA1, RSA-SHA256, and PLAINTEXT.
10
+ #
11
+ # @api public
12
+ # @example Register a custom signature method
13
+ # SimpleOAuth::Signature.register("HMAC-SHA512") do |secret, signature_base|
14
+ # SimpleOAuth::Signature.encode_base64(
15
+ # OpenSSL::HMAC.digest("SHA512", secret, signature_base)
16
+ # )
17
+ # end
18
+ #
19
+ # @example Check if a signature method is registered
20
+ # SimpleOAuth::Signature.registered?("HMAC-SHA1") # => true
21
+ # SimpleOAuth::Signature.registered?("CUSTOM") # => false
22
+ module Signature
23
+ # Registry of signature method implementations
24
+ @registry = {}
25
+
26
+ class << self
27
+ # Registers a custom signature method
28
+ #
29
+ # @api public
30
+ # @param name [String] the signature method name (e.g., "HMAC-SHA512")
31
+ # @param rsa [Boolean] whether this method uses RSA (raw consumer_secret as key)
32
+ # @yield [secret, signature_base] block that computes the signature
33
+ # @yieldparam secret [String] the signing secret (or PEM key for RSA methods)
34
+ # @yieldparam signature_base [String] the signature base string
35
+ # @yieldreturn [String] the computed signature
36
+ # @return [void]
37
+ # @example
38
+ # SimpleOAuth::Signature.register("HMAC-SHA512") do |secret, base|
39
+ # SimpleOAuth::Signature.encode_base64(
40
+ # OpenSSL::HMAC.digest("SHA512", secret, base)
41
+ # )
42
+ # end
43
+ def register(name, rsa: false, &block)
44
+ @registry[normalize_name(name)] = {implementation: block, rsa: rsa}
45
+ end
46
+
47
+ # Checks if a signature method is registered
48
+ #
49
+ # @api public
50
+ # @param name [String] the signature method name
51
+ # @return [Boolean] true if the method is registered
52
+ # @example
53
+ # SimpleOAuth::Signature.registered?("HMAC-SHA1") # => true
54
+ def registered?(name)
55
+ @registry.key?(normalize_name(name))
56
+ end
57
+
58
+ # Returns list of registered signature method names
59
+ #
60
+ # @api public
61
+ # @return [Array<String>] registered method names
62
+ # @example
63
+ # SimpleOAuth::Signature.methods # => ["hmac_sha1", "hmac_sha256", "rsa_sha1", "plaintext"]
64
+ def methods
65
+ @registry.keys
66
+ end
67
+
68
+ # Checks if a signature method uses RSA (raw key instead of escaped secret)
69
+ #
70
+ # @api public
71
+ # @param name [String] the signature method name
72
+ # @return [Boolean] true if the method uses RSA
73
+ # @example
74
+ # SimpleOAuth::Signature.rsa?("RSA-SHA1") # => true
75
+ # SimpleOAuth::Signature.rsa?("HMAC-SHA1") # => false
76
+ def rsa?(name)
77
+ @registry.dig(normalize_name(name), :rsa) || false
78
+ end
79
+
80
+ # Computes a signature using the specified method
81
+ #
82
+ # @api public
83
+ # @param name [String] the signature method name
84
+ # @param secret [String] the signing secret
85
+ # @param signature_base [String] the signature base string
86
+ # @return [String] the computed signature
87
+ # @raise [ArgumentError] if the signature method is not registered
88
+ # @example
89
+ # SimpleOAuth::Signature.sign("HMAC-SHA1", "secret&token", "GET&url&params")
90
+ def sign(name, secret, signature_base)
91
+ normalized = normalize_name(name)
92
+ entry = @registry.fetch(normalized) do
93
+ raise ArgumentError, "Unknown signature method: #{name}. " \
94
+ "Registered methods: #{@registry.keys.join(", ")}"
95
+ end
96
+ entry.fetch(:implementation).call(secret, signature_base)
97
+ end
98
+
99
+ # Unregisters a signature method (useful for testing)
100
+ #
101
+ # @api public
102
+ # @param name [String] the signature method name to remove
103
+ # @return [void]
104
+ # @example
105
+ # SimpleOAuth::Signature.unregister("HMAC-SHA512")
106
+ def unregister(name)
107
+ @registry.delete(normalize_name(name))
108
+ end
109
+
110
+ # Resets the registry to only built-in methods (useful for testing)
111
+ #
112
+ # @api public
113
+ # @return [void]
114
+ # @example
115
+ # SimpleOAuth::Signature.reset!
116
+ def reset!
117
+ @registry.clear
118
+ register_builtin_methods
119
+ end
120
+
121
+ # Encodes binary data as Base64 without newlines
122
+ #
123
+ # @api public
124
+ # @param data [String] binary data to encode
125
+ # @return [String] Base64-encoded string without newlines
126
+ # @example
127
+ # SimpleOAuth::Signature.encode_base64("\x01\x02\x03")
128
+ # # => "AQID"
129
+ def encode_base64(data)
130
+ Base64.strict_encode64(data)
131
+ end
132
+
133
+ private
134
+
135
+ # Normalizes signature method name for registry lookup
136
+ #
137
+ # @api private
138
+ # @param name [String] the signature method name
139
+ # @return [String] normalized name (lowercase, dashes to underscores)
140
+ def normalize_name(name)
141
+ name.to_s.downcase.tr("-", "_")
142
+ end
143
+
144
+ # Registers the built-in OAuth signature methods
145
+ #
146
+ # @api private
147
+ # @return [void]
148
+ def register_builtin_methods
149
+ register_hmac_methods
150
+ register_rsa_methods
151
+ register_plaintext_method
152
+ end
153
+
154
+ # Registers HMAC-based signature methods
155
+ #
156
+ # @api private
157
+ # @return [void]
158
+ def register_hmac_methods
159
+ %w[SHA1 SHA256].each do |digest|
160
+ register("HMAC-#{digest}") do |secret, signature_base|
161
+ encode_base64(OpenSSL::HMAC.digest(digest, secret, signature_base))
162
+ end
163
+ end
164
+ end
165
+
166
+ # Registers RSA-based signature methods
167
+ #
168
+ # @api private
169
+ # @return [void]
170
+ def register_rsa_methods
171
+ %w[SHA1 SHA256].each do |digest|
172
+ register("RSA-#{digest}", rsa: true) do |private_key_pem, signature_base|
173
+ private_key = OpenSSL::PKey::RSA.new(private_key_pem)
174
+ encode_base64(private_key.sign(digest, signature_base))
175
+ end
176
+ end
177
+ end
178
+
179
+ # Registers the PLAINTEXT signature method
180
+ #
181
+ # @api private
182
+ # @return [void]
183
+ def register_plaintext_method
184
+ register("PLAINTEXT") { |secret, _| secret }
185
+ end
186
+ end
187
+
188
+ # Initialize built-in methods on load
189
+ register_builtin_methods
190
+ end
191
+ end
@@ -0,0 +1,5 @@
1
+ # OAuth 1.0 header generation library
2
+ module SimpleOauth
3
+ # The current version of the SimpleOAuth gem
4
+ VERSION = "0.4.0".freeze
5
+ end
data/lib/simple_oauth.rb CHANGED
@@ -1 +1,30 @@
1
- require 'simple_oauth/header'
1
+ require_relative "simple_oauth/header"
2
+ require_relative "simple_oauth/version"
3
+
4
+ # OAuth 1.0 header generation and parsing library
5
+ #
6
+ # SimpleOAuth provides a simple interface for building and verifying
7
+ # OAuth 1.0 Authorization headers per RFC 5849.
8
+ #
9
+ # @example Building an OAuth header
10
+ # header = SimpleOAuth::Header.new(
11
+ # :get,
12
+ # "https://api.example.com/resource",
13
+ # {status: "Hello"},
14
+ # consumer_key: "key",
15
+ # consumer_secret: "secret"
16
+ # )
17
+ # header.to_s # => "OAuth oauth_consumer_key=\"key\", ..."
18
+ #
19
+ # @example Parsing an OAuth header
20
+ # parsed = SimpleOAuth::Header.parse('OAuth oauth_consumer_key="key"')
21
+ # # => {consumer_key: "key"}
22
+ #
23
+ # @see https://tools.ietf.org/html/rfc5849 RFC 5849 - The OAuth 1.0 Protocol
24
+ module SimpleOAuth
25
+ # Error raised when parsing a malformed OAuth Authorization header
26
+ class ParseError < StandardError; end
27
+
28
+ # Error raised when invalid options are passed to Header
29
+ # (defined in header.rb, exported here for convenience)
30
+ end