simple_oauth 0.5.0 → 0.5.2
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 +4 -4
- data/CHANGELOG.md +20 -0
- data/README.md +4 -2
- data/lib/simple_oauth/header/class_methods.rb +16 -1
- data/lib/simple_oauth/header/params.rb +5 -1
- data/lib/simple_oauth/header.rb +66 -5
- data/lib/simple_oauth/version.rb +1 -1
- data/sig/simple_oauth/header/class_methods.rbs +3 -0
- data/sig/simple_oauth.rbs +12 -0
- metadata +2 -3
- data/sig/uri_ext.rbs +0 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 88c89c6dff93b31b6279da0d85c0b719cc94bac966356ae1783702ed3cbfe8e5
|
|
4
|
+
data.tar.gz: 1febe046539f65e64a5b3d6b787282e5ce91178bf1617f97ce6a08a5a159318e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1f2b608cbf969b4983f29c746a5e892b0d5b1d321a6e044b8c01f645be8733aade948f5410a0654a6ece51778050cdb5a846e7ca02d70c1bd53cdf4d943aab8d
|
|
7
|
+
data.tar.gz: 5c76e78b8b537367e00833e063f9a23bacd5e8ea00563e5e41536e0ee223f4c257032001411059380a6f3b39ab883e5253304a12e59eba2da4d88d6ebe130af7
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
## [0.5.2] - 2026-11-14
|
|
2
|
+
|
|
3
|
+
### Security
|
|
4
|
+
|
|
5
|
+
* `Header#valid?` no longer verifies with whatever signature method the received header claims. A server verifying RSA signatures with a client's public key, as the README showed, accepted forged requests signed with that public key as an HMAC-SHA1 or PLAINTEXT secret. Reported by Rathnadeepan (HotWater, [@HotWater-015](https://github.com/HotWater-015))
|
|
6
|
+
* Given a `signature_method`, the header must claim that method
|
|
7
|
+
* Without one, the header must claim an RSA method for an RSA key and an HMAC method for any other secret
|
|
8
|
+
* PLAINTEXT signatures verify only when the caller names `PLAINTEXT`
|
|
9
|
+
|
|
10
|
+
### Deprecated
|
|
11
|
+
|
|
12
|
+
* Calling `Header#valid?` without a `signature_method`, which a later major version will require
|
|
13
|
+
|
|
14
|
+
## [0.5.1] - 2026-09-12
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
* Build the signature base string with a String conversion that every supported Ruby offers; `Header#url` called `URI::Generic#to_str`, which arrived in `uri` 0.13, so it raised `NoMethodError` on a stock Ruby 3.2, the oldest version the gem claims to support
|
|
19
|
+
* Sign a parameter that carries no value, such as a bare `?flag` in the query string or the `c2` of the RFC 5849 Section 3.4.1.3.1 example, as `name=` rather than dropping it from the signature base string; a request carrying one signed differently than the server computes, so it was rejected
|
|
20
|
+
|
|
1
21
|
## [0.5.0] - 2026-09-12
|
|
2
22
|
|
|
3
23
|
### Added
|
data/README.md
CHANGED
|
@@ -155,16 +155,18 @@ parsed = SimpleOAuth::Header.parse_query("oauth_consumer_key=key&status=hello")
|
|
|
155
155
|
header = SimpleOAuth::Header.new(:get, request_url, params, authorization_header)
|
|
156
156
|
|
|
157
157
|
# Verify the signature
|
|
158
|
-
header.valid?(consumer_secret: "secret", token_secret: "token_secret")
|
|
158
|
+
header.valid?(consumer_secret: "secret", token_secret: "token_secret", signature_method: "HMAC-SHA1")
|
|
159
159
|
# => true
|
|
160
160
|
```
|
|
161
161
|
|
|
162
|
+
Name the signature method you expect, and a header claiming any other method is not valid. The method a request claims is chosen by whoever sent it, so verifying with it unchecked would let anyone holding an RSA public key pass it off as an HMAC or PLAINTEXT secret. Leaving `signature_method` out is deprecated: the header must then claim an RSA method for an RSA key and an HMAC method for any other secret. PLAINTEXT signatures are accepted only when you name `PLAINTEXT`, and they send the secrets themselves, so accept them only over TLS.
|
|
163
|
+
|
|
162
164
|
Verifying compares signatures in constant time and leaves the header's own options untouched, so the secrets stay with the caller.
|
|
163
165
|
|
|
164
166
|
RSA signatures verify with the client's public key, which is all a server has:
|
|
165
167
|
|
|
166
168
|
```ruby
|
|
167
|
-
header.valid?(consumer_secret: File.read("client_public_key.pem"))
|
|
169
|
+
header.valid?(consumer_secret: File.read("client_public_key.pem"), signature_method: "RSA-SHA1")
|
|
168
170
|
```
|
|
169
171
|
|
|
170
172
|
Custom signature methods that cannot be verified by recomputing the signature register a `verify` block:
|
|
@@ -71,7 +71,7 @@ module SimpleOAuth
|
|
|
71
71
|
def from_request(request, oauth = {})
|
|
72
72
|
uri = request.uri || raise(ArgumentError, "The request has no URI")
|
|
73
73
|
body = request.body
|
|
74
|
-
return new(request.method, uri,
|
|
74
|
+
return new(request.method, uri, form_params(body), oauth) if form_encoded?(request)
|
|
75
75
|
|
|
76
76
|
no_params = {} #: Header::request_params
|
|
77
77
|
new(request.method, uri, no_params, oauth, body)
|
|
@@ -117,6 +117,21 @@ module SimpleOAuth
|
|
|
117
117
|
|
|
118
118
|
private
|
|
119
119
|
|
|
120
|
+
# Parses a form-encoded body into the parameter pairs to sign
|
|
121
|
+
#
|
|
122
|
+
# A parameter with no value, such as the "c2" of the RFC 5849 Section 3.4.1.3.1
|
|
123
|
+
# example, is signed with an empty value rather than dropped.
|
|
124
|
+
#
|
|
125
|
+
# @api private
|
|
126
|
+
# @param body [String, nil] the form-encoded body
|
|
127
|
+
# @return [Array<Array(String, String)>] the parameter pairs
|
|
128
|
+
def form_params(body)
|
|
129
|
+
CGI.parse(body.to_s).flat_map do |key, values|
|
|
130
|
+
# A parameter with no value still makes one pair, carrying an empty value
|
|
131
|
+
(values.empty? ? [""] : values).map { |value| [key, value] }
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
120
135
|
# Checks whether a request carries a form-encoded body
|
|
121
136
|
#
|
|
122
137
|
# @api private
|
|
@@ -33,11 +33,15 @@ module SimpleOAuth
|
|
|
33
33
|
|
|
34
34
|
# Extracts query parameters from the request URL
|
|
35
35
|
#
|
|
36
|
+
# A parameter with no value, such as the "c2" of the RFC 5849 Section 3.4.1.3.1
|
|
37
|
+
# example, is signed with an empty value rather than dropped.
|
|
38
|
+
#
|
|
36
39
|
# @api private
|
|
37
40
|
# @return [Array<Array>] URL query parameters as key-value pairs
|
|
38
41
|
def url_params
|
|
39
42
|
CGI.parse(@uri.query || "").flat_map do |key, values|
|
|
40
|
-
|
|
43
|
+
# A parameter with no value still makes one pair, carrying an empty value
|
|
44
|
+
(values.empty? ? [""] : values.sort).map { |value| [key, value] }
|
|
41
45
|
end
|
|
42
46
|
end
|
|
43
47
|
|
data/lib/simple_oauth/header.rb
CHANGED
|
@@ -23,6 +23,9 @@ module SimpleOAuth
|
|
|
23
23
|
# Default signature method per RFC 5849
|
|
24
24
|
DEFAULT_SIGNATURE_METHOD = "HMAC-SHA1".freeze
|
|
25
25
|
|
|
26
|
+
# The signature method that sends the secrets themselves, which verifying accepts only when named
|
|
27
|
+
PLAINTEXT_SIGNATURE_METHOD = "PLAINTEXT".freeze
|
|
28
|
+
|
|
26
29
|
# OAuth version
|
|
27
30
|
OAUTH_VERSION = "1.0".freeze
|
|
28
31
|
|
|
@@ -102,7 +105,9 @@ module SimpleOAuth
|
|
|
102
105
|
# header.url
|
|
103
106
|
# # => "https://api.example.com/path"
|
|
104
107
|
def url
|
|
105
|
-
|
|
108
|
+
# String() takes whichever conversion the installed uri defines: it gained to_str in
|
|
109
|
+
# 0.13, and the uri that ships with Ruby 3.2 offers only to_s
|
|
110
|
+
String(@uri.dup.tap { |uri| uri.query = nil })
|
|
106
111
|
end
|
|
107
112
|
|
|
108
113
|
# Returns the OAuth Authorization header string
|
|
@@ -120,18 +125,30 @@ module SimpleOAuth
|
|
|
120
125
|
|
|
121
126
|
# Validates the signature in the header against the provided secrets
|
|
122
127
|
#
|
|
128
|
+
# The method a received header claims is chosen by whoever sent it, so it is checked before it
|
|
129
|
+
# is used. Given a signature_method, the header must claim that method. Without one, the header
|
|
130
|
+
# must claim a method that fits the key: an RSA method for an RSA key, and an HMAC method for any
|
|
131
|
+
# other secret. PLAINTEXT is accepted only when named, as it sends the secrets themselves.
|
|
132
|
+
# Without that check, a sender holding only an RSA public key could sign with it as an HMAC or
|
|
133
|
+
# PLAINTEXT secret.
|
|
134
|
+
#
|
|
123
135
|
# @api public
|
|
124
|
-
# @param secrets [Hash] the consumer_secret and token_secret for validation
|
|
136
|
+
# @param secrets [Hash] the consumer_secret and token_secret for validation, and the
|
|
137
|
+
# signature_method the header must use, which a later major version will require
|
|
125
138
|
# @return [Boolean] true if the signature is valid, false otherwise
|
|
139
|
+
# @raise [ArgumentError] if the expected signature method is not registered
|
|
126
140
|
# @note When the header was built with a body, the signed oauth_body_hash must match that body,
|
|
127
141
|
# so a tampered body fails verification even though its signature covers the claimed hash
|
|
128
142
|
# @example
|
|
129
143
|
# parsed_header = SimpleOAuth::Header.new(:get, url, {}, authorization_header)
|
|
130
|
-
# parsed_header.valid?(consumer_secret: "secret", token_secret: "token_secret"
|
|
144
|
+
# parsed_header.valid?(consumer_secret: "secret", token_secret: "token_secret",
|
|
145
|
+
# signature_method: "HMAC-SHA1")
|
|
131
146
|
# # => true
|
|
132
147
|
def valid?(secrets = {})
|
|
133
|
-
|
|
134
|
-
|
|
148
|
+
verifying = options.merge(secrets)
|
|
149
|
+
signature_method_acceptable?(verifying, secrets[:signature_method]) && body_hash_valid? &&
|
|
150
|
+
Signature.verify(options.fetch(:signature_method), signing_key(verifying), signature_base,
|
|
151
|
+
options.fetch(:signature))
|
|
135
152
|
end
|
|
136
153
|
|
|
137
154
|
# Returns the OAuth attributes including the signature
|
|
@@ -213,6 +230,50 @@ module SimpleOAuth
|
|
|
213
230
|
Signature.rsa?(options.fetch(:signature_method)) ? options[:consumer_secret] : secret(options)
|
|
214
231
|
end
|
|
215
232
|
|
|
233
|
+
# Checks that the header claims a signature method the verifier accepts
|
|
234
|
+
#
|
|
235
|
+
# @api private
|
|
236
|
+
# @param verifying [Hash] the header's options merged with the verifier's secrets
|
|
237
|
+
# @param expected [String, nil] the signature method the verifier expects, if it names one
|
|
238
|
+
# @return [Boolean] true if the claimed method is the expected one, or fits the key
|
|
239
|
+
# @raise [ArgumentError] if the expected signature method is not registered
|
|
240
|
+
def signature_method_acceptable?(verifying, expected)
|
|
241
|
+
claimed = options[:signature_method].to_s
|
|
242
|
+
return expected_signature_method?(claimed, expected) if expected
|
|
243
|
+
|
|
244
|
+
warn("SimpleOAuth::Header#valid? without a signature_method is deprecated. " \
|
|
245
|
+
"Name the method to verify with, such as signature_method: \"HMAC-SHA1\"", category: :deprecated)
|
|
246
|
+
Signature.registered?(claimed) && !PLAINTEXT_SIGNATURE_METHOD.casecmp?(claimed) &&
|
|
247
|
+
Signature.rsa?(claimed).eql?(rsa_key?(verifying[:consumer_secret]))
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Checks that the header claims the signature method the verifier names
|
|
251
|
+
#
|
|
252
|
+
# Signature method names are compared without regard to case, as the registry looks them up.
|
|
253
|
+
#
|
|
254
|
+
# @api private
|
|
255
|
+
# @param claimed [String] the signature method the header claims
|
|
256
|
+
# @param expected [String] the signature method the verifier expects
|
|
257
|
+
# @return [Boolean] true if the header claims the expected method
|
|
258
|
+
# @raise [ArgumentError] if the expected signature method is not registered
|
|
259
|
+
def expected_signature_method?(claimed, expected)
|
|
260
|
+
raise ArgumentError, "Unknown signature method: #{expected}" unless Signature.registered?(expected)
|
|
261
|
+
|
|
262
|
+
claimed.casecmp?(expected)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Checks whether a verifying key is an RSA key
|
|
266
|
+
#
|
|
267
|
+
# @api private
|
|
268
|
+
# @param key [String, nil] the consumer_secret the verifier gave
|
|
269
|
+
# @return [Boolean] true if the key parses as an RSA key
|
|
270
|
+
def rsa_key?(key)
|
|
271
|
+
OpenSSL::PKey::RSA.new(key.to_s)
|
|
272
|
+
true
|
|
273
|
+
rescue OpenSSL::PKey::PKeyError
|
|
274
|
+
false
|
|
275
|
+
end
|
|
276
|
+
|
|
216
277
|
# Checks the body against the oauth_body_hash the header carries
|
|
217
278
|
#
|
|
218
279
|
# A header parsed from a request claims a body hash that its signature covers, so the claim must be
|
data/lib/simple_oauth/version.rb
CHANGED
|
@@ -28,6 +28,9 @@ module SimpleOAuth
|
|
|
28
28
|
|
|
29
29
|
private
|
|
30
30
|
|
|
31
|
+
# Parses a form-encoded body into the parameter pairs to sign
|
|
32
|
+
def form_params: (String? body) -> Array[[String, String]]
|
|
33
|
+
|
|
31
34
|
# Checks whether a request carries a form-encoded body
|
|
32
35
|
def form_encoded?: (Header::_Request request) -> bool
|
|
33
36
|
|
data/sig/simple_oauth.rbs
CHANGED
|
@@ -58,6 +58,9 @@ module SimpleOAuth
|
|
|
58
58
|
# Valid keys when parsing OAuth parameters (ATTRIBUTE_KEYS + signature)
|
|
59
59
|
PARSE_KEYS: Array[Symbol]
|
|
60
60
|
|
|
61
|
+
# The signature method that sends the secrets themselves, which verifying accepts only when named
|
|
62
|
+
PLAINTEXT_SIGNATURE_METHOD: String
|
|
63
|
+
|
|
61
64
|
# What from_request needs from a request object, such as a Net::HTTPRequest
|
|
62
65
|
interface _Request
|
|
63
66
|
def method: () -> String
|
|
@@ -144,6 +147,15 @@ module SimpleOAuth
|
|
|
144
147
|
# The key for signing and verifying
|
|
145
148
|
def signing_key: (oauth_options options) -> String?
|
|
146
149
|
|
|
150
|
+
# Checks that the header claims a signature method the verifier accepts
|
|
151
|
+
def signature_method_acceptable?: (oauth_options verifying, String? expected) -> bool
|
|
152
|
+
|
|
153
|
+
# Checks that the header claims the signature method the verifier names
|
|
154
|
+
def expected_signature_method?: (String claimed, String expected) -> bool
|
|
155
|
+
|
|
156
|
+
# Checks whether a verifying key is an RSA key
|
|
157
|
+
def rsa_key?: (String? key) -> bool
|
|
158
|
+
|
|
147
159
|
# Checks the body against the oauth_body_hash the header carries
|
|
148
160
|
def body_hash_valid?: () -> bool
|
|
149
161
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: simple_oauth
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.5.
|
|
4
|
+
version: 0.5.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Steve Richert
|
|
@@ -72,7 +72,6 @@ files:
|
|
|
72
72
|
- sig/simple_oauth/parser.rbs
|
|
73
73
|
- sig/simple_oauth/signature.rbs
|
|
74
74
|
- sig/strscan.rbs
|
|
75
|
-
- sig/uri_ext.rbs
|
|
76
75
|
homepage: https://github.com/laserlemon/simple_oauth
|
|
77
76
|
licenses:
|
|
78
77
|
- MIT
|
|
@@ -96,7 +95,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
96
95
|
- !ruby/object:Gem::Version
|
|
97
96
|
version: '0'
|
|
98
97
|
requirements: []
|
|
99
|
-
rubygems_version: 4.0.
|
|
98
|
+
rubygems_version: 4.0.24
|
|
100
99
|
specification_version: 4
|
|
101
100
|
summary: Simply builds and verifies OAuth headers
|
|
102
101
|
test_files: []
|