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.
- checksums.yaml +5 -5
- data/.rubocop.yml +51 -49
- data/CHANGELOG.md +21 -0
- data/LICENSE.md +18 -17
- data/README.md +136 -42
- data/Rakefile +27 -18
- data/Steepfile +18 -0
- data/lib/simple_oauth/encoding.rb +48 -0
- data/lib/simple_oauth/errors.rb +7 -0
- data/lib/simple_oauth/header/class_methods.rb +99 -0
- data/lib/simple_oauth/header.rb +218 -77
- data/lib/simple_oauth/parser.rb +107 -0
- data/lib/simple_oauth/signature.rb +191 -0
- data/lib/simple_oauth/version.rb +5 -0
- data/lib/simple_oauth.rb +30 -1
- data/mutant.yml +17 -0
- data/sig/matchdata_ext.rbs +6 -0
- data/sig/openssl_ext.rbs +9 -0
- data/sig/simple_oauth/header/class_methods.rbs +26 -0
- data/sig/simple_oauth/parser.rbs +24 -0
- data/sig/simple_oauth/signature.rbs +57 -0
- data/sig/simple_oauth.rbs +158 -0
- data/sig/strscan.rbs +9 -0
- data/sig/uri_rfc2396_parser.rbs +10 -0
- metadata +49 -23
- data/.gitignore +0 -10
- data/.rspec +0 -2
- data/.travis.yml +0 -21
- data/Gemfile +0 -17
- data/simple_oauth.gemspec +0 -15
data/lib/simple_oauth/header.rb
CHANGED
|
@@ -1,133 +1,274 @@
|
|
|
1
|
-
require
|
|
2
|
-
require
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
14
|
+
# OAuth header scheme prefix
|
|
15
|
+
OAUTH_SCHEME = "OAuth".freeze
|
|
9
16
|
|
|
10
|
-
|
|
17
|
+
# Prefix for OAuth parameters
|
|
18
|
+
OAUTH_PREFIX = "oauth_".freeze
|
|
11
19
|
|
|
12
|
-
|
|
20
|
+
# Default signature method per RFC 5849
|
|
21
|
+
DEFAULT_SIGNATURE_METHOD = "HMAC-SHA1".freeze
|
|
13
22
|
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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
|
-
|
|
32
|
-
|
|
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
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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 =
|
|
51
|
-
@uri.scheme = @uri.scheme.downcase
|
|
52
|
-
@uri.normalize!
|
|
53
|
-
@uri.fragment = nil
|
|
84
|
+
@uri = normalize_uri(url)
|
|
54
85
|
@params = params
|
|
55
|
-
@
|
|
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
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
140
|
+
header_attributes.merge(oauth_signature: signature)
|
|
78
141
|
end
|
|
79
142
|
|
|
80
|
-
|
|
143
|
+
private
|
|
81
144
|
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
167
|
+
self.class.parse(oauth)
|
|
93
168
|
end
|
|
94
169
|
end
|
|
95
170
|
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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
|
-
|
|
105
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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
|
-
|
|
114
|
-
|
|
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
|
-
|
|
118
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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
|
-
|
|
126
|
-
|
|
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
|
-
|
|
130
|
-
|
|
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¶ms")
|
|
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
|
data/lib/simple_oauth.rb
CHANGED
|
@@ -1 +1,30 @@
|
|
|
1
|
-
|
|
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
|