oauth2 1.4.9 → 2.0.17
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
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +706 -88
- data/CITATION.cff +20 -0
- data/CODE_OF_CONDUCT.md +24 -23
- data/CONTRIBUTING.md +229 -0
- data/FUNDING.md +77 -0
- data/{LICENSE → LICENSE.txt} +2 -2
- data/OIDC.md +158 -0
- data/README.md +1513 -251
- data/REEK +0 -0
- data/RUBOCOP.md +71 -0
- data/SECURITY.md +21 -0
- data/lib/oauth2/access_token.rb +276 -39
- data/lib/oauth2/authenticator.rb +45 -8
- data/lib/oauth2/client.rb +406 -129
- data/lib/oauth2/error.rb +59 -24
- data/lib/oauth2/filtered_attributes.rb +52 -0
- data/lib/oauth2/response.rb +127 -36
- data/lib/oauth2/strategy/assertion.rb +68 -40
- data/lib/oauth2/strategy/auth_code.rb +25 -4
- data/lib/oauth2/strategy/client_credentials.rb +3 -3
- data/lib/oauth2/strategy/implicit.rb +17 -2
- data/lib/oauth2/strategy/password.rb +14 -4
- data/lib/oauth2/version.rb +1 -59
- data/lib/oauth2.rb +79 -12
- data/sig/oauth2/access_token.rbs +25 -0
- data/sig/oauth2/authenticator.rbs +22 -0
- data/sig/oauth2/client.rbs +52 -0
- data/sig/oauth2/error.rbs +8 -0
- data/sig/oauth2/filtered_attributes.rbs +6 -0
- data/sig/oauth2/response.rbs +18 -0
- data/sig/oauth2/strategy.rbs +34 -0
- data/sig/oauth2/version.rbs +5 -0
- data/sig/oauth2.rbs +9 -0
- data.tar.gz.sig +0 -0
- metadata +336 -89
- metadata.gz.sig +0 -0
- data/lib/oauth2/mac_token.rb +0 -130
- data/spec/fixtures/README.md +0 -11
- data/spec/fixtures/RS256/jwtRS256.key +0 -51
- data/spec/fixtures/RS256/jwtRS256.key.pub +0 -14
- data/spec/helper.rb +0 -33
- data/spec/oauth2/access_token_spec.rb +0 -218
- data/spec/oauth2/authenticator_spec.rb +0 -86
- data/spec/oauth2/client_spec.rb +0 -556
- data/spec/oauth2/mac_token_spec.rb +0 -122
- data/spec/oauth2/response_spec.rb +0 -96
- data/spec/oauth2/strategy/assertion_spec.rb +0 -113
- data/spec/oauth2/strategy/auth_code_spec.rb +0 -108
- data/spec/oauth2/strategy/base_spec.rb +0 -7
- data/spec/oauth2/strategy/client_credentials_spec.rb +0 -71
- data/spec/oauth2/strategy/implicit_spec.rb +0 -28
- data/spec/oauth2/strategy/password_spec.rb +0 -58
- data/spec/oauth2/version_spec.rb +0 -23
data/REEK
ADDED
File without changes
|
data/RUBOCOP.md
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
# RuboCop Usage Guide
|
2
|
+
|
3
|
+
## Overview
|
4
|
+
|
5
|
+
A tale of two RuboCop plugin gems.
|
6
|
+
|
7
|
+
### RuboCop Gradual
|
8
|
+
|
9
|
+
This project uses `rubocop_gradual` instead of vanilla RuboCop for code style checking. The `rubocop_gradual` tool allows for gradual adoption of RuboCop rules by tracking violations in a lock file.
|
10
|
+
|
11
|
+
### RuboCop LTS
|
12
|
+
|
13
|
+
This project uses `rubocop-lts` to ensure, on a best-effort basis, compatibility with Ruby >= 1.9.2.
|
14
|
+
RuboCop rules are meticulously configured by the `rubocop-lts` family of gems to ensure that a project is compatible with a specific version of Ruby. See: https://rubocop-lts.gitlab.io for more.
|
15
|
+
|
16
|
+
## Checking RuboCop Violations
|
17
|
+
|
18
|
+
To check for RuboCop violations in this project, always use:
|
19
|
+
|
20
|
+
```bash
|
21
|
+
bundle exec rake rubocop_gradual:check
|
22
|
+
```
|
23
|
+
|
24
|
+
**Do not use** the standard RuboCop commands like:
|
25
|
+
- `bundle exec rubocop`
|
26
|
+
- `rubocop`
|
27
|
+
|
28
|
+
## Understanding the Lock File
|
29
|
+
|
30
|
+
The `.rubocop_gradual.lock` file tracks all current RuboCop violations in the project. This allows the team to:
|
31
|
+
|
32
|
+
1. Prevent new violations while gradually fixing existing ones
|
33
|
+
2. Track progress on code style improvements
|
34
|
+
3. Ensure CI builds don't fail due to pre-existing violations
|
35
|
+
|
36
|
+
## Common Commands
|
37
|
+
|
38
|
+
- **Check violations**
|
39
|
+
- `bundle exec rake rubocop_gradual`
|
40
|
+
- `bundle exec rake rubocop_gradual:check`
|
41
|
+
- **(Safe) Autocorrect violations, and update lockfile if no new violations**
|
42
|
+
- `bundle exec rake rubocop_gradual:autocorrect`
|
43
|
+
- **Force update the lock file (w/o autocorrect) to match violations present in code**
|
44
|
+
- `bundle exec rake rubocop_gradual:force_update`
|
45
|
+
|
46
|
+
## Workflow
|
47
|
+
|
48
|
+
1. Before submitting a PR, run `bundle exec rake rubocop_gradual:autocorrect`
|
49
|
+
a. or just the default `bundle exec rake`, as autocorrection is a pre-requisite of the default task.
|
50
|
+
2. If there are new violations, either:
|
51
|
+
- Fix them in your code
|
52
|
+
- Run `bundle exec rake rubocop_gradual:force_update` to update the lock file (only for violations you can't fix immediately)
|
53
|
+
3. Commit the updated `.rubocop_gradual.lock` file along with your changes
|
54
|
+
|
55
|
+
## Never add inline RuboCop disables
|
56
|
+
|
57
|
+
Do not add inline `rubocop:disable` / `rubocop:enable` comments anywhere in the codebase (including specs, except when following the few existing `rubocop:disable` patterns for a rule already being disabled elsewhere in the code). We handle exceptions in two supported ways:
|
58
|
+
|
59
|
+
- Permanent/structural exceptions: prefer adjusting the RuboCop configuration (e.g., in `.rubocop.yml`) to exclude a rule for a path or file pattern when it makes sense project-wide.
|
60
|
+
- Temporary exceptions while improving code: record the current violations in `.rubocop_gradual.lock` via the gradual workflow:
|
61
|
+
- `bundle exec rake rubocop_gradual:autocorrect` (preferred; will autocorrect what it can and update the lock only if no new violations were introduced)
|
62
|
+
- If needed, `bundle exec rake rubocop_gradual:force_update` (as a last resort when you cannot fix the newly reported violations immediately)
|
63
|
+
|
64
|
+
In general, treat the rules as guidance to follow; fix violations rather than ignore them. For example, RSpec conventions in this project expect `described_class` to be used in specs that target a specific class under test.
|
65
|
+
|
66
|
+
## Benefits of rubocop_gradual
|
67
|
+
|
68
|
+
- Allows incremental adoption of code style rules
|
69
|
+
- Prevents CI failures due to pre-existing violations
|
70
|
+
- Provides a clear record of code style debt
|
71
|
+
- Enables focused efforts on improving code quality over time
|
data/SECURITY.md
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# Security Policy
|
2
|
+
|
3
|
+
## Supported Versions
|
4
|
+
|
5
|
+
| Version | Supported |
|
6
|
+
|----------|-----------|
|
7
|
+
| 1.latest | ✅ |
|
8
|
+
|
9
|
+
## Security contact information
|
10
|
+
|
11
|
+
To report a security vulnerability, please use the
|
12
|
+
[Tidelift security contact](https://tidelift.com/security).
|
13
|
+
Tidelift will coordinate the fix and disclosure.
|
14
|
+
|
15
|
+
## Additional Support
|
16
|
+
|
17
|
+
If you are interested in support for versions older than the latest release,
|
18
|
+
please consider sponsoring the project / maintainer @ https://liberapay.com/pboling/donate,
|
19
|
+
or find other sponsorship links in the [README].
|
20
|
+
|
21
|
+
[README]: README.md
|
data/lib/oauth2/access_token.rb
CHANGED
@@ -1,59 +1,182 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# :nocov:
|
4
|
+
begin
|
5
|
+
# The first version of hashie that has a version file was 1.1.0
|
6
|
+
# The first version of hashie that required the version file at runtime was 3.2.0
|
7
|
+
# If it has already been loaded then this is very low cost, as Kernel.require uses maintains a cache
|
8
|
+
# If this it hasn't this will work to get it loaded, and then we will be able to use
|
9
|
+
# defined?(Hashie::Version)
|
10
|
+
# as a test.
|
11
|
+
# TODO: get rid this mess when we drop Hashie < 3.2, as Hashie will self-load its version then
|
12
|
+
require "hashie/version"
|
13
|
+
rescue LoadError
|
14
|
+
nil
|
15
|
+
end
|
16
|
+
# :nocov:
|
17
|
+
|
3
18
|
module OAuth2
|
4
|
-
class AccessToken
|
5
|
-
|
6
|
-
|
19
|
+
class AccessToken # rubocop:disable Metrics/ClassLength
|
20
|
+
TOKEN_KEYS_STR = %w[access_token id_token token accessToken idToken].freeze
|
21
|
+
TOKEN_KEYS_SYM = %i[access_token id_token token accessToken idToken].freeze
|
22
|
+
TOKEN_KEY_LOOKUP = TOKEN_KEYS_STR + TOKEN_KEYS_SYM
|
23
|
+
|
24
|
+
include FilteredAttributes
|
25
|
+
|
26
|
+
attr_reader :client, :token, :expires_in, :expires_at, :expires_latency, :params
|
27
|
+
attr_accessor :options, :refresh_token, :response
|
28
|
+
filtered_attributes :token, :refresh_token
|
7
29
|
|
8
|
-
# Should these methods be deprecated?
|
9
30
|
class << self
|
10
31
|
# Initializes an AccessToken from a Hash
|
11
32
|
#
|
12
|
-
# @param [Client] the OAuth2::Client instance
|
13
|
-
# @param [Hash] a hash
|
14
|
-
# @
|
33
|
+
# @param [OAuth2::Client] client the OAuth2::Client instance
|
34
|
+
# @param [Hash] hash a hash containing the token and other properties
|
35
|
+
# @option hash [String] 'access_token' the access token value
|
36
|
+
# @option hash [String] 'id_token' alternative key for the access token value
|
37
|
+
# @option hash [String] 'token' alternative key for the access token value
|
38
|
+
# @option hash [String] 'refresh_token' (optional) the refresh token value
|
39
|
+
# @option hash [Integer, String] 'expires_in' (optional) number of seconds until token expires
|
40
|
+
# @option hash [Integer, String] 'expires_at' (optional) epoch time in seconds when token expires
|
41
|
+
# @option hash [Integer, String] 'expires_latency' (optional) seconds to reduce token validity by
|
42
|
+
#
|
43
|
+
# @return [OAuth2::AccessToken] the initialized AccessToken
|
44
|
+
#
|
45
|
+
# @note The method will use the first found token key in the following order:
|
46
|
+
# 'access_token', 'id_token', 'token' (or their symbolic versions)
|
47
|
+
# @note If multiple token keys are present, a warning will be issued unless
|
48
|
+
# OAuth2.config.silence_extra_tokens_warning is true
|
49
|
+
# @note If no token keys are present, a warning will be issued unless
|
50
|
+
# OAuth2.config.silence_no_tokens_warning is true
|
51
|
+
# @note For "soon-to-expire"/"clock-skew" functionality see the `:expires_latency` option.
|
52
|
+
# @note If snaky key conversion is being used, token_name needs to match the converted key.
|
53
|
+
#
|
54
|
+
# @example
|
55
|
+
# hash = { 'access_token' => 'token_value', 'refresh_token' => 'refresh_value' }
|
56
|
+
# access_token = OAuth2::AccessToken.from_hash(client, hash)
|
15
57
|
def from_hash(client, hash)
|
16
|
-
|
17
|
-
|
58
|
+
fresh = hash.dup
|
59
|
+
# If token_name is present, then use that key name
|
60
|
+
key =
|
61
|
+
if fresh.key?(:token_name)
|
62
|
+
t_key = fresh[:token_name]
|
63
|
+
no_tokens_warning(fresh, t_key)
|
64
|
+
t_key
|
65
|
+
else
|
66
|
+
# Otherwise, if one of the supported default keys is present, use whichever has precedence
|
67
|
+
supported_keys = TOKEN_KEY_LOOKUP & fresh.keys
|
68
|
+
t_key = supported_keys[0]
|
69
|
+
extra_tokens_warning(supported_keys, t_key)
|
70
|
+
t_key
|
71
|
+
end
|
72
|
+
# :nocov:
|
73
|
+
# TODO: Get rid of this branching logic when dropping Hashie < v3.2
|
74
|
+
token = if !defined?(Hashie::VERSION) # i.e. <= "1.1.0"; the first Hashie to ship with a VERSION constant
|
75
|
+
warn("snaky_hash and oauth2 will drop support for Hashie v0 in the next major version. Please upgrade to a modern Hashie.")
|
76
|
+
# There is a bug in Hashie v0, which is accounts for.
|
77
|
+
fresh.delete(key) || fresh[key] || ""
|
78
|
+
else
|
79
|
+
fresh.delete(key) || ""
|
80
|
+
end
|
81
|
+
# :nocov:
|
82
|
+
new(client, token, fresh)
|
18
83
|
end
|
19
84
|
|
20
85
|
# Initializes an AccessToken from a key/value application/x-www-form-urlencoded string
|
21
86
|
#
|
22
87
|
# @param [Client] client the OAuth2::Client instance
|
23
88
|
# @param [String] kvform the application/x-www-form-urlencoded string
|
24
|
-
# @return [AccessToken] the
|
89
|
+
# @return [AccessToken] the initialized AccessToken
|
25
90
|
def from_kvform(client, kvform)
|
26
91
|
from_hash(client, Rack::Utils.parse_query(kvform))
|
27
92
|
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
# Having too many is sus, and may lead to bugs. Having none is fine (e.g. refresh flow doesn't need a token).
|
97
|
+
def extra_tokens_warning(supported_keys, key)
|
98
|
+
return if OAuth2.config.silence_extra_tokens_warning
|
99
|
+
return if supported_keys.length <= 1
|
100
|
+
|
101
|
+
warn("OAuth2::AccessToken.from_hash: `hash` contained more than one 'token' key (#{supported_keys}); using #{key.inspect}.")
|
102
|
+
end
|
103
|
+
|
104
|
+
def no_tokens_warning(hash, key)
|
105
|
+
return if OAuth2.config.silence_no_tokens_warning
|
106
|
+
return if key && hash.key?(key)
|
107
|
+
|
108
|
+
warn(%[
|
109
|
+
OAuth2::AccessToken#from_hash key mismatch.
|
110
|
+
Custom token_name (#{key}) is not found in (#{hash.keys})
|
111
|
+
You may need to set `snaky: false`. See inline documentation for more info.
|
112
|
+
])
|
113
|
+
end
|
28
114
|
end
|
29
115
|
|
30
|
-
#
|
116
|
+
# Initialize an AccessToken
|
117
|
+
#
|
118
|
+
# @note For "soon-to-expire"/"clock-skew" functionality see the `:expires_latency` option.
|
119
|
+
# @note If no token is provided, the AccessToken will be considered invalid.
|
120
|
+
# This is to prevent the possibility of a token being accidentally
|
121
|
+
# created with no token value.
|
122
|
+
# If you want to create an AccessToken with no token value,
|
123
|
+
# you can pass in an empty string or nil for the token value.
|
124
|
+
# If you want to create an AccessToken with no token value and
|
125
|
+
# no refresh token, you can pass in an empty string or nil for the
|
126
|
+
# token value and nil for the refresh token, and `raise_errors: false`.
|
31
127
|
#
|
32
128
|
# @param [Client] client the OAuth2::Client instance
|
33
|
-
# @param [String] token the Access Token value
|
129
|
+
# @param [String] token the Access Token value (optional, may not be used in refresh flows)
|
34
130
|
# @param [Hash] opts the options to create the Access Token with
|
35
131
|
# @option opts [String] :refresh_token (nil) the refresh_token value
|
36
132
|
# @option opts [FixNum, String] :expires_in (nil) the number of seconds in which the AccessToken will expire
|
37
133
|
# @option opts [FixNum, String] :expires_at (nil) the epoch time in seconds in which AccessToken will expire
|
38
|
-
# @option opts [
|
39
|
-
#
|
134
|
+
# @option opts [FixNum, String] :expires_latency (nil) the number of seconds by which AccessToken validity will be reduced to offset latency, @version 2.0+
|
135
|
+
# @option opts [Symbol, Hash, or callable] :mode (:header) the transmission mode of the Access Token parameter value:
|
136
|
+
# either one of :header, :body or :query; or a Hash with verb symbols as keys mapping to one of these symbols
|
137
|
+
# (e.g., {get: :query, post: :header, delete: :header}); or a callable that accepts a request-verb parameter
|
138
|
+
# and returns one of these three symbols.
|
40
139
|
# @option opts [String] :header_format ('Bearer %s') the string format to use for the Authorization header
|
140
|
+
#
|
141
|
+
# @example Verb-dependent Hash mode
|
142
|
+
# # Send token in query for GET, in header for POST/DELETE, in body for PUT/PATCH
|
143
|
+
# OAuth2::AccessToken.new(client, token, mode: {get: :query, post: :header, delete: :header, put: :body, patch: :body})
|
41
144
|
# @option opts [String] :param_name ('access_token') the parameter name to use for transmission of the
|
42
145
|
# Access Token value in :body or :query transmission mode
|
146
|
+
# @option opts [String] :token_name (nil) the name of the response parameter that identifies the access token
|
147
|
+
# When nil one of TOKEN_KEY_LOOKUP will be used
|
43
148
|
def initialize(client, token, opts = {})
|
44
149
|
@client = client
|
45
150
|
@token = token.to_s
|
46
151
|
opts = opts.dup
|
47
|
-
[
|
152
|
+
%i[refresh_token expires_in expires_at expires_latency].each do |arg|
|
48
153
|
instance_variable_set("@#{arg}", opts.delete(arg) || opts.delete(arg.to_s))
|
49
154
|
end
|
50
|
-
@
|
155
|
+
no_tokens = (@token.nil? || @token.empty?) && (@refresh_token.nil? || @refresh_token.empty?)
|
156
|
+
if no_tokens
|
157
|
+
if @client.options[:raise_errors]
|
158
|
+
raise Error.new({
|
159
|
+
error: "OAuth2::AccessToken has no token",
|
160
|
+
error_description: "Options are: #{opts.inspect}",
|
161
|
+
})
|
162
|
+
elsif !OAuth2.config.silence_no_tokens_warning
|
163
|
+
warn("OAuth2::AccessToken has no token")
|
164
|
+
end
|
165
|
+
end
|
166
|
+
# @option opts [Fixnum, String] :expires is deprecated
|
167
|
+
@expires_in ||= opts.delete("expires")
|
51
168
|
@expires_in &&= @expires_in.to_i
|
52
169
|
@expires_at &&= convert_expires_at(@expires_at)
|
53
|
-
@
|
54
|
-
@
|
55
|
-
|
56
|
-
|
170
|
+
@expires_latency &&= @expires_latency.to_i
|
171
|
+
@expires_at ||= Time.now.to_i + @expires_in if @expires_in && !@expires_in.zero?
|
172
|
+
@expires_at -= @expires_latency if @expires_latency
|
173
|
+
@options = {
|
174
|
+
mode: opts.delete(:mode) || :header,
|
175
|
+
header_format: opts.delete(:header_format) || "Bearer %s",
|
176
|
+
param_name: opts.delete(:param_name) || "access_token",
|
177
|
+
}
|
178
|
+
@options[:token_name] = opts.delete(:token_name) if opts.key?(:token_name)
|
179
|
+
|
57
180
|
@params = opts
|
58
181
|
end
|
59
182
|
|
@@ -64,40 +187,131 @@ module OAuth2
|
|
64
187
|
@params[key]
|
65
188
|
end
|
66
189
|
|
67
|
-
# Whether
|
190
|
+
# Whether the token expires
|
68
191
|
#
|
69
192
|
# @return [Boolean]
|
70
193
|
def expires?
|
71
194
|
!!@expires_at
|
72
195
|
end
|
73
196
|
|
74
|
-
#
|
197
|
+
# Check if token is expired
|
75
198
|
#
|
76
|
-
# @return [Boolean]
|
199
|
+
# @return [Boolean] true if the token is expired, false otherwise
|
77
200
|
def expired?
|
78
|
-
expires? && (expires_at
|
201
|
+
expires? && (expires_at <= Time.now.to_i)
|
79
202
|
end
|
80
203
|
|
81
204
|
# Refreshes the current Access Token
|
82
205
|
#
|
83
|
-
# @
|
84
|
-
# @
|
85
|
-
|
86
|
-
|
206
|
+
# @param [Hash] params additional params to pass to the refresh token request
|
207
|
+
# @param [Hash] access_token_opts options that will be passed to the AccessToken initialization
|
208
|
+
#
|
209
|
+
# @yield [opts] The block to modify the refresh token request options
|
210
|
+
# @yieldparam [Hash] opts The options hash that can be modified
|
211
|
+
#
|
212
|
+
# @return [OAuth2::AccessToken] a new AccessToken instance
|
213
|
+
#
|
214
|
+
# @note current token's options are carried over to the new AccessToken
|
215
|
+
def refresh(params = {}, access_token_opts = {}, &block)
|
216
|
+
raise OAuth2::Error.new({error: "A refresh_token is not available"}) unless refresh_token
|
87
217
|
|
88
|
-
params[:grant_type] =
|
218
|
+
params[:grant_type] = "refresh_token"
|
89
219
|
params[:refresh_token] = refresh_token
|
90
|
-
new_token = @client.get_token(params)
|
220
|
+
new_token = @client.get_token(params, access_token_opts, &block)
|
91
221
|
new_token.options = options
|
92
|
-
|
222
|
+
if new_token.refresh_token
|
223
|
+
# Keep it if there is one
|
224
|
+
else
|
225
|
+
new_token.refresh_token = refresh_token
|
226
|
+
end
|
93
227
|
new_token
|
94
228
|
end
|
229
|
+
# A compatibility alias
|
230
|
+
# @note does not modify the receiver, so bang is not the default method
|
231
|
+
alias_method :refresh!, :refresh
|
232
|
+
|
233
|
+
# Revokes the token at the authorization server
|
234
|
+
#
|
235
|
+
# @param [Hash] params additional parameters to be sent during revocation
|
236
|
+
# @option params [String, Symbol, nil] :token_type_hint ('access_token' or 'refresh_token') hint about which token to revoke
|
237
|
+
# @option params [Symbol] :token_method (:post_with_query_string) overrides OAuth2::Client#options[:token_method]
|
238
|
+
#
|
239
|
+
# @yield [req] The block is passed the request being made, allowing customization
|
240
|
+
# @yieldparam [Faraday::Request] req The request object that can be modified
|
241
|
+
#
|
242
|
+
# @return [OAuth2::Response] OAuth2::Response instance
|
243
|
+
#
|
244
|
+
# @api public
|
245
|
+
#
|
246
|
+
# @raise [OAuth2::Error] if token_type_hint is invalid or the specified token is not available
|
247
|
+
#
|
248
|
+
# @note If the token passed to the request
|
249
|
+
# is an access token, the server MAY revoke the respective refresh
|
250
|
+
# token as well.
|
251
|
+
# @note If the token passed to the request
|
252
|
+
# is a refresh token and the authorization server supports the
|
253
|
+
# revocation of access tokens, then the authorization server SHOULD
|
254
|
+
# also invalidate all access tokens based on the same authorization
|
255
|
+
# grant
|
256
|
+
# @note If the server responds with HTTP status code 503, your code must
|
257
|
+
# assume the token still exists and may retry after a reasonable delay.
|
258
|
+
# The server may include a "Retry-After" header in the response to
|
259
|
+
# indicate how long the service is expected to be unavailable to the
|
260
|
+
# requesting client.
|
261
|
+
#
|
262
|
+
# @see https://datatracker.ietf.org/doc/html/rfc7009
|
263
|
+
# @see https://datatracker.ietf.org/doc/html/rfc7009#section-2.1
|
264
|
+
def revoke(params = {}, &block)
|
265
|
+
token_type_hint_orig = params.delete(:token_type_hint)
|
266
|
+
token_type_hint = nil
|
267
|
+
revoke_token = case token_type_hint_orig
|
268
|
+
when "access_token", :access_token
|
269
|
+
token_type_hint = "access_token"
|
270
|
+
token
|
271
|
+
when "refresh_token", :refresh_token
|
272
|
+
token_type_hint = "refresh_token"
|
273
|
+
refresh_token
|
274
|
+
when nil
|
275
|
+
if token
|
276
|
+
token_type_hint = "access_token"
|
277
|
+
token
|
278
|
+
elsif refresh_token
|
279
|
+
token_type_hint = "refresh_token"
|
280
|
+
refresh_token
|
281
|
+
end
|
282
|
+
else
|
283
|
+
raise OAuth2::Error.new({error: "token_type_hint must be one of [nil, :refresh_token, :access_token], so if you need something else consider using a subclass or entirely custom AccessToken class."})
|
284
|
+
end
|
285
|
+
raise OAuth2::Error.new({error: "#{token_type_hint || "unknown token type"} is not available for revoking"}) unless revoke_token && !revoke_token.empty?
|
286
|
+
|
287
|
+
@client.revoke_token(revoke_token, token_type_hint, params, &block)
|
288
|
+
end
|
289
|
+
# A compatibility alias
|
290
|
+
# @note does not modify the receiver, so bang is not the default method
|
291
|
+
alias_method :revoke!, :revoke
|
95
292
|
|
96
293
|
# Convert AccessToken to a hash which can be used to rebuild itself with AccessToken.from_hash
|
97
294
|
#
|
295
|
+
# @note Don't return expires_latency because it has already been deducted from expires_at
|
296
|
+
#
|
98
297
|
# @return [Hash] a hash of AccessToken property values
|
99
298
|
def to_hash
|
100
|
-
|
299
|
+
hsh = {
|
300
|
+
access_token: token,
|
301
|
+
refresh_token: refresh_token,
|
302
|
+
expires_at: expires_at,
|
303
|
+
mode: options[:mode],
|
304
|
+
header_format: options[:header_format],
|
305
|
+
param_name: options[:param_name],
|
306
|
+
}
|
307
|
+
hsh[:token_name] = options[:token_name] if options.key?(:token_name)
|
308
|
+
# TODO: Switch when dropping Ruby < 2.5 support
|
309
|
+
# params.transform_keys(&:to_sym) # Ruby 2.5 only
|
310
|
+
# Old Ruby transform_keys alternative:
|
311
|
+
sheesh = @params.each_with_object({}) { |(k, v), memo|
|
312
|
+
memo[k.to_sym] = v
|
313
|
+
}
|
314
|
+
sheesh.merge(hsh)
|
101
315
|
end
|
102
316
|
|
103
317
|
# Make a request with the Access Token
|
@@ -105,9 +319,18 @@ module OAuth2
|
|
105
319
|
# @param [Symbol] verb the HTTP request method
|
106
320
|
# @param [String] path the HTTP URL path of the request
|
107
321
|
# @param [Hash] opts the options to make the request with
|
108
|
-
# @
|
322
|
+
# @option opts [Hash] :params additional URL parameters
|
323
|
+
# @option opts [Hash, String] :body the request body
|
324
|
+
# @option opts [Hash] :headers request headers
|
325
|
+
#
|
326
|
+
# @yield [req] The block to modify the request
|
327
|
+
# @yieldparam [Faraday::Request] req The request object that can be modified
|
328
|
+
#
|
329
|
+
# @return [OAuth2::Response] the response from the request
|
330
|
+
#
|
331
|
+
# @see OAuth2::Client#request
|
109
332
|
def request(verb, path, opts = {}, &block)
|
110
|
-
configure_authentication!(opts)
|
333
|
+
configure_authentication!(opts, verb)
|
111
334
|
@client.request(verb, path, opts, &block)
|
112
335
|
end
|
113
336
|
|
@@ -148,17 +371,31 @@ module OAuth2
|
|
148
371
|
|
149
372
|
# Get the headers hash (includes Authorization token)
|
150
373
|
def headers
|
151
|
-
{
|
374
|
+
{"Authorization" => options[:header_format] % token}
|
152
375
|
end
|
153
376
|
|
154
377
|
private
|
155
378
|
|
156
|
-
def configure_authentication!(opts)
|
157
|
-
|
379
|
+
def configure_authentication!(opts, verb)
|
380
|
+
mode_opt = options[:mode]
|
381
|
+
mode =
|
382
|
+
if mode_opt.respond_to?(:call)
|
383
|
+
mode_opt.call(verb)
|
384
|
+
elsif mode_opt.is_a?(Hash)
|
385
|
+
key = verb.to_sym
|
386
|
+
# Try symbol key first, then string key; default to :header when missing
|
387
|
+
mode_opt[key] || mode_opt[key.to_s] || :header
|
388
|
+
else
|
389
|
+
mode_opt
|
390
|
+
end
|
391
|
+
|
392
|
+
case mode
|
158
393
|
when :header
|
159
394
|
opts[:headers] ||= {}
|
160
395
|
opts[:headers].merge!(headers)
|
161
396
|
when :query
|
397
|
+
# OAuth 2.1 note: Bearer tokens in the query string are omitted from the spec due to security risks.
|
398
|
+
# Prefer the default :header mode whenever possible.
|
162
399
|
opts[:params] ||= {}
|
163
400
|
opts[:params][options[:param_name]] = token
|
164
401
|
when :body
|
@@ -166,11 +403,11 @@ module OAuth2
|
|
166
403
|
if opts[:body].is_a?(Hash)
|
167
404
|
opts[:body][options[:param_name]] = token
|
168
405
|
else
|
169
|
-
opts[:body]
|
406
|
+
opts[:body] += "&#{options[:param_name]}=#{token}"
|
170
407
|
end
|
171
408
|
# @todo support for multi-part (file uploads)
|
172
409
|
else
|
173
|
-
raise("invalid :mode option of #{
|
410
|
+
raise("invalid :mode option of #{mode}")
|
174
411
|
end
|
175
412
|
end
|
176
413
|
|
data/lib/oauth2/authenticator.rb
CHANGED
@@ -1,11 +1,26 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "base64"
|
4
4
|
|
5
5
|
module OAuth2
|
6
|
+
# Builds and applies client authentication to token and revoke requests.
|
7
|
+
#
|
8
|
+
# Depending on the selected mode, credentials are applied as Basic Auth
|
9
|
+
# headers, request body parameters, or only the client_id is sent (TLS).
|
6
10
|
class Authenticator
|
11
|
+
include FilteredAttributes
|
12
|
+
|
13
|
+
# @return [Symbol, String] Authentication mode (e.g., :basic_auth, :request_body, :tls_client_auth, :private_key_jwt)
|
14
|
+
# @return [String, nil] Client identifier
|
15
|
+
# @return [String, nil] Client secret (filtered in inspected output)
|
7
16
|
attr_reader :mode, :id, :secret
|
17
|
+
filtered_attributes :secret
|
8
18
|
|
19
|
+
# Create a new Authenticator
|
20
|
+
#
|
21
|
+
# @param [String, nil] id Client identifier
|
22
|
+
# @param [String, nil] secret Client secret
|
23
|
+
# @param [Symbol, String] mode Authentication mode
|
9
24
|
def initialize(id, secret, mode)
|
10
25
|
@id = id
|
11
26
|
@secret = secret
|
@@ -14,7 +29,7 @@ module OAuth2
|
|
14
29
|
|
15
30
|
# Apply the request credentials used to authenticate to the Authorization Server
|
16
31
|
#
|
17
|
-
# Depending on configuration, this might be as request params or as an
|
32
|
+
# Depending on the configuration, this might be as request params or as an
|
18
33
|
# Authorization header.
|
19
34
|
#
|
20
35
|
# User-provided params and header take precedence.
|
@@ -36,35 +51,57 @@ module OAuth2
|
|
36
51
|
end
|
37
52
|
end
|
38
53
|
|
54
|
+
# Encodes a Basic Authorization header value for the provided credentials.
|
55
|
+
#
|
56
|
+
# @param [String] user The client identifier
|
57
|
+
# @param [String] password The client secret
|
58
|
+
# @return [String] The value to use for the Authorization header
|
39
59
|
def self.encode_basic_auth(user, password)
|
40
|
-
|
60
|
+
"Basic #{Base64.strict_encode64("#{user}:#{password}")}"
|
41
61
|
end
|
42
62
|
|
43
63
|
private
|
44
64
|
|
45
65
|
# Adds client_id and client_secret request parameters if they are not
|
46
66
|
# already set.
|
67
|
+
#
|
68
|
+
# @param [Hash] params Request parameters
|
69
|
+
# @return [Hash] Updated parameters including client_id and client_secret
|
47
70
|
def apply_params_auth(params)
|
48
|
-
|
71
|
+
result = {}
|
72
|
+
result["client_id"] = id unless id.nil?
|
73
|
+
result["client_secret"] = secret unless secret.nil?
|
74
|
+
result.merge(params)
|
49
75
|
end
|
50
76
|
|
51
|
-
# When using schemes that don't require the client_secret to be passed
|
77
|
+
# When using schemes that don't require the client_secret to be passed (e.g., TLS Client Auth),
|
52
78
|
# we don't want to send the secret
|
79
|
+
#
|
80
|
+
# @param [Hash] params Request parameters
|
81
|
+
# @return [Hash] Updated parameters including only client_id
|
53
82
|
def apply_client_id(params)
|
54
|
-
|
83
|
+
result = {}
|
84
|
+
result["client_id"] = id unless id.nil?
|
85
|
+
result.merge(params)
|
55
86
|
end
|
56
87
|
|
57
88
|
# Adds an `Authorization` header with Basic Auth credentials if and only if
|
58
89
|
# it is not already set in the params.
|
90
|
+
#
|
91
|
+
# @param [Hash] params Request parameters (may include :headers)
|
92
|
+
# @return [Hash] Updated parameters with Authorization header
|
59
93
|
def apply_basic_auth(params)
|
60
94
|
headers = params.fetch(:headers, {})
|
61
95
|
headers = basic_auth_header.merge(headers)
|
62
|
-
params.merge(:
|
96
|
+
params.merge(headers: headers)
|
63
97
|
end
|
64
98
|
|
99
|
+
# Build the Basic Authorization header.
|
100
|
+
#
|
65
101
|
# @see https://datatracker.ietf.org/doc/html/rfc2617#section-2
|
102
|
+
# @return [Hash] Header hash containing the Authorization entry
|
66
103
|
def basic_auth_header
|
67
|
-
{
|
104
|
+
{"Authorization" => self.class.encode_basic_auth(id, secret)}
|
68
105
|
end
|
69
106
|
end
|
70
107
|
end
|