Skip to content

Commit 7cf97b3

Browse files
committed
Merge branch 'feat/revoke_token' into 'main'
Add IETF RFC 7009 Token Revocation Closes #224 See merge request oauth-xx/oauth2!647
2 parents fd098bb + 20a5f2b commit 7cf97b3

File tree

8 files changed

+476
-105
lines changed

8 files changed

+476
-105
lines changed

.rubocop_gradual.lock

Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,14 @@
33
[66, 5, 20, "ThreadSafety/ClassInstanceVariable: Avoid class instance variables.", 2485198147],
44
[78, 5, 74, "Style/InvertibleUnlessCondition: Prefer `if Gem.rubygems_version >= Gem::Version.new(\"2.7.0\")` over `unless Gem.rubygems_version < Gem::Version.new(\"2.7.0\")`.", 2453573257]
55
],
6-
"lib/oauth2.rb:1956148869": [
7-
[35, 5, 21, "ThreadSafety/ClassAndModuleAttributes: Avoid mutating class and module attributes.", 622027168],
6+
"lib/oauth2.rb:4176768025": [
87
[38, 11, 7, "ThreadSafety/ClassInstanceVariable: Avoid class instance variables.", 651502127]
98
],
10-
"lib/oauth2/access_token.rb:2233632404": [
9+
"lib/oauth2/access_token.rb:3471244990": [
1110
[49, 13, 5, "Style/IdenticalConditionalBranches: Move `t_key` out of the conditional.", 183811513],
1211
[55, 13, 5, "Style/IdenticalConditionalBranches: Move `t_key` out of the conditional.", 183811513]
1312
],
14-
"lib/oauth2/authenticator.rb:3711266135": [
13+
"lib/oauth2/authenticator.rb:63639854": [
1514
[42, 5, 113, "Style/ClassMethodsDefinitions: Use `class << self` to define a class method.", 734523108]
1615
],
1716
"lib/oauth2/filtered_attributes.rb:1202323815": [
@@ -32,11 +31,15 @@
3231
[130, 3, 52, "Gemspec/DependencyVersion: Dependency version specification is required.", 3163430777],
3332
[131, 3, 48, "Gemspec/DependencyVersion: Dependency version specification is required.", 425065368]
3433
],
35-
"spec/oauth2/access_token_spec.rb:3473606468": [
34+
"spec/oauth2/access_token_spec.rb:443932125": [
3635
[3, 1, 34, "RSpec/SpecFilePathFormat: Spec path should end with `o_auth2/access_token*_spec.rb`.", 1972107547],
37-
[780, 13, 25, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 770233088],
38-
[850, 9, 101, "Style/ClassMethodsDefinitions: Use `class << self` to define a class method.", 3022740639],
39-
[854, 9, 79, "Style/ClassMethodsDefinitions: Use `class << self` to define a class method.", 2507338967]
36+
[392, 142, 40, "Lint/LiteralInInterpolation: Literal interpolation detected.", 4210228387],
37+
[400, 142, 40, "Lint/LiteralInInterpolation: Literal interpolation detected.", 4210228387],
38+
[606, 142, 20, "Lint/LiteralInInterpolation: Literal interpolation detected.", 304063511],
39+
[632, 142, 20, "Lint/LiteralInInterpolation: Literal interpolation detected.", 304063511],
40+
[781, 13, 25, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 770233088],
41+
[851, 9, 101, "Style/ClassMethodsDefinitions: Use `class << self` to define a class method.", 3022740639],
42+
[855, 9, 79, "Style/ClassMethodsDefinitions: Use `class << self` to define a class method.", 2507338967]
4043
],
4144
"spec/oauth2/authenticator_spec.rb:853320290": [
4245
[3, 1, 36, "RSpec/SpecFilePathFormat: Spec path should end with `o_auth2/authenticator*_spec.rb`.", 819808017],
@@ -45,26 +48,24 @@
4548
[69, 15, 38, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 1480816240],
4649
[79, 13, 23, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 2314399065]
4750
],
48-
"spec/oauth2/client_spec.rb:2085440011": [
51+
"spec/oauth2/client_spec.rb:1326196445": [
4952
[6, 1, 29, "RSpec/SpecFilePathFormat: Spec path should end with `o_auth2/client*_spec.rb`.", 439549885],
50-
[174, 7, 492, "RSpec/NoExpectationExample: No expectation found in this example.", 1272021224],
51-
[193, 7, 592, "RSpec/NoExpectationExample: No expectation found in this example.", 3428877205],
52-
[206, 15, 20, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 2320605227],
53-
[221, 15, 20, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 1276531672],
54-
[236, 15, 43, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 1383956904],
55-
[251, 15, 43, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 3376202107],
56-
[829, 5, 360, "RSpec/NoExpectationExample: No expectation found in this example.", 536201463],
57-
[838, 5, 461, "RSpec/NoExpectationExample: No expectation found in this example.", 3392600621],
58-
[849, 5, 340, "RSpec/NoExpectationExample: No expectation found in this example.", 244592251],
59-
[894, 63, 2, "RSpec/BeEq: Prefer `be` over `eq`.", 5860785],
60-
[939, 11, 99, "Style/ClassMethodsDefinitions: Use `class << self` to define a class method.", 3084776886],
61-
[943, 11, 82, "Style/ClassMethodsDefinitions: Use `class << self` to define a class method.", 1524553529],
62-
[951, 7, 89, "RSpec/NoExpectationExample: No expectation found in this example.", 4609419],
63-
[1039, 11, 99, "Style/ClassMethodsDefinitions: Use `class << self` to define a class method.", 3084776886],
64-
[1043, 11, 82, "Style/ClassMethodsDefinitions: Use `class << self` to define a class method.", 1524553529],
65-
[1123, 17, 12, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 664794325],
66-
[1148, 5, 459, "RSpec/NoExpectationExample: No expectation found in this example.", 2216851076],
67-
[1158, 7, 450, "RSpec/NoExpectationExample: No expectation found in this example.", 2619808549]
53+
[175, 7, 492, "RSpec/NoExpectationExample: No expectation found in this example.", 1272021224],
54+
[194, 7, 592, "RSpec/NoExpectationExample: No expectation found in this example.", 3428877205],
55+
[207, 15, 20, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 2320605227],
56+
[222, 15, 20, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 1276531672],
57+
[237, 15, 43, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 1383956904],
58+
[252, 15, 43, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 3376202107],
59+
[830, 5, 360, "RSpec/NoExpectationExample: No expectation found in this example.", 536201463],
60+
[839, 5, 461, "RSpec/NoExpectationExample: No expectation found in this example.", 3392600621],
61+
[850, 5, 340, "RSpec/NoExpectationExample: No expectation found in this example.", 244592251],
62+
[895, 63, 2, "RSpec/BeEq: Prefer `be` over `eq`.", 5860785],
63+
[940, 11, 99, "Style/ClassMethodsDefinitions: Use `class << self` to define a class method.", 3084776886],
64+
[944, 11, 82, "Style/ClassMethodsDefinitions: Use `class << self` to define a class method.", 1524553529],
65+
[952, 7, 89, "RSpec/NoExpectationExample: No expectation found in this example.", 4609419],
66+
[1040, 11, 99, "Style/ClassMethodsDefinitions: Use `class << self` to define a class method.", 3084776886],
67+
[1044, 11, 82, "Style/ClassMethodsDefinitions: Use `class << self` to define a class method.", 1524553529],
68+
[1124, 17, 12, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 664794325]
6869
],
6970
"spec/oauth2/error_spec.rb:1209122273": [
7071
[23, 1, 28, "RSpec/SpecFilePathFormat: Spec path should end with `o_auth2/error*_spec.rb`.", 3385870076],

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ and this project adheres to [Semantic Versioning v2](https://semver.org/spec/v2.
2424
- Specify the parameter name that identifies the access token
2525
- [!645](https://gitlab.com/oauth-xx/oauth2/-/merge_requests/645) - Add `OAuth2::OAUTH_DEBUG` constant, based on `ENV["OAUTH_DEBUG"] (@pboling)
2626
- [!646](https://gitlab.com/oauth-xx/oauth2/-/merge_requests/646) - Add `OAuth2.config.silence_extra_tokens_warning`, default: false (@pboling)
27+
- Added IETF RFC 7009 Token Revocation compliant `OAuth2::Client#revoke_token` and `OAuth2::AccessToken#revoke`
28+
- See: https://datatracker.ietf.org/doc/html/rfc7009
2729
### Changed
2830
- Default value of `OAuth2.config.silence_extra_tokens_warning` was `false`, now `true`
2931
- Gem releases are now cryptographically signed, with a 20-year cert (@pboling)

lib/oauth2.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ module OAuth2
3232
)
3333
@config = DEFAULT_CONFIG.dup
3434
class << self
35-
attr_accessor :config
35+
attr_reader :config
3636
end
3737
def configure
3838
yield @config

lib/oauth2/access_token.rb

Lines changed: 91 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ class << self
3434
# @note If no token keys are present, a warning will be issued unless
3535
# OAuth2.config.silence_no_tokens_warning is true
3636
# @note For "soon-to-expire"/"clock-skew" functionality see the `:expires_latency` option.
37-
# @mote If snaky key conversion is being used, token_name needs to match the converted key.
37+
# @note If snaky key conversion is being used, token_name needs to match the converted key.
3838
#
3939
# @example
4040
# hash = { 'access_token' => 'token_value', 'refresh_token' => 'refresh_value' }
@@ -125,8 +125,10 @@ def initialize(client, token, opts = {})
125125
no_tokens = (@token.nil? || @token.empty?) && (@refresh_token.nil? || @refresh_token.empty?)
126126
if no_tokens
127127
if @client.options[:raise_errors]
128-
error = Error.new(opts)
129-
raise(error)
128+
raise Error.new({
129+
error: "OAuth2::AccessToken has no token",
130+
error_description: "Options are: #{opts.inspect}",
131+
})
130132
elsif !OAuth2.config.silence_no_tokens_warning
131133
warn("OAuth2::AccessToken has no token")
132134
end
@@ -155,33 +157,40 @@ def [](key)
155157
@params[key]
156158
end
157159

158-
# Whether or not the token expires
160+
# Whether the token expires
159161
#
160162
# @return [Boolean]
161163
def expires?
162164
!!@expires_at
163165
end
164166

165-
# Whether or not the token is expired
167+
# Check if token is expired
166168
#
167-
# @return [Boolean]
169+
# @return [Boolean] true if the token is expired, false otherwise
168170
def expired?
169171
expires? && (expires_at <= Time.now.to_i)
170172
end
171173

172174
# Refreshes the current Access Token
173175
#
174-
# @return [AccessToken] a new AccessToken
175-
# @note options should be carried over to the new AccessToken
176-
def refresh(params = {}, access_token_opts = {})
177-
raise("A refresh_token is not available") unless refresh_token
176+
# @param [Hash] params additional params to pass to the refresh token request
177+
# @param [Hash] access_token_opts options that will be passed to the AccessToken initialization
178+
#
179+
# @yield [opts] The block to modify the refresh token request options
180+
# @yieldparam [Hash] opts The options hash that can be modified
181+
#
182+
# @return [OAuth2::AccessToken] a new AccessToken instance
183+
#
184+
# @note current token's options are carried over to the new AccessToken
185+
def refresh(params = {}, access_token_opts = {}, &block)
186+
raise OAuth2::Error.new({error: "A refresh_token is not available"}) unless refresh_token
178187

179188
params[:grant_type] = "refresh_token"
180189
params[:refresh_token] = refresh_token
181-
new_token = @client.get_token(params, access_token_opts)
190+
new_token = @client.get_token(params, access_token_opts, &block)
182191
new_token.options = options
183192
if new_token.refresh_token
184-
# Keep it, if there is one
193+
# Keep it if there is one
185194
else
186195
new_token.refresh_token = refresh_token
187196
end
@@ -191,6 +200,66 @@ def refresh(params = {}, access_token_opts = {})
191200
# @note does not modify the receiver, so bang is not the default method
192201
alias_method :refresh!, :refresh
193202

203+
# Revokes the token at the authorization server
204+
#
205+
# @param [Hash] params additional parameters to be sent during revocation
206+
# @option params [String, Symbol, nil] :token_type_hint ('access_token' or 'refresh_token') hint about which token to revoke
207+
# @option params [Symbol] :token_method (:post_with_query_string) overrides OAuth2::Client#options[:token_method]
208+
#
209+
# @yield [req] The block is passed the request being made, allowing customization
210+
# @yieldparam [Faraday::Request] req The request object that can be modified
211+
#
212+
# @return [OAuth2::Response] OAuth2::Response instance
213+
#
214+
# @api public
215+
#
216+
# @raise [OAuth2::Error] if token_type_hint is invalid or the specified token is not available
217+
#
218+
# @note If the token passed to the request
219+
# is an access token, the server MAY revoke the respective refresh
220+
# token as well.
221+
# @note If the token passed to the request
222+
# is a refresh token and the authorization server supports the
223+
# revocation of access tokens, then the authorization server SHOULD
224+
# also invalidate all access tokens based on the same authorization
225+
# grant
226+
# @note If the server responds with HTTP status code 503, your code must
227+
# assume the token still exists and may retry after a reasonable delay.
228+
# The server may include a "Retry-After" header in the response to
229+
# indicate how long the service is expected to be unavailable to the
230+
# requesting client.
231+
#
232+
# @see https://datatracker.ietf.org/doc/html/rfc7009
233+
# @see https://datatracker.ietf.org/doc/html/rfc7009#section-2.1
234+
def revoke(params = {}, &block)
235+
token_type_hint_orig = params.delete(:token_type_hint)
236+
token_type_hint = nil
237+
revoke_token = case token_type_hint_orig
238+
when "access_token", :access_token
239+
token_type_hint = "access_token"
240+
token
241+
when "refresh_token", :refresh_token
242+
token_type_hint = "refresh_token"
243+
refresh_token
244+
when nil
245+
if token
246+
token_type_hint = "access_token"
247+
token
248+
elsif refresh_token
249+
token_type_hint = "refresh_token"
250+
refresh_token
251+
end
252+
else
253+
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."})
254+
end
255+
raise OAuth2::Error.new({error: "#{token_type_hint || "unknown token type"} is not available for revoking"}) unless revoke_token && !revoke_token.empty?
256+
257+
@client.revoke_token(revoke_token, token_type_hint, params, &block)
258+
end
259+
# A compatibility alias
260+
# @note does not modify the receiver, so bang is not the default method
261+
alias_method :revoke!, :revoke
262+
194263
# Convert AccessToken to a hash which can be used to rebuild itself with AccessToken.from_hash
195264
#
196265
# @note Don't return expires_latency because it has already been deducted from expires_at
@@ -220,7 +289,16 @@ def to_hash
220289
# @param [Symbol] verb the HTTP request method
221290
# @param [String] path the HTTP URL path of the request
222291
# @param [Hash] opts the options to make the request with
223-
# @see Client#request
292+
# @option opts [Hash] :params additional URL parameters
293+
# @option opts [Hash, String] :body the request body
294+
# @option opts [Hash] :headers request headers
295+
#
296+
# @yield [req] The block to modify the request
297+
# @yieldparam [Faraday::Request] req The request object that can be modified
298+
#
299+
# @return [OAuth2::Response] the response from the request
300+
#
301+
# @see OAuth2::Client#request
224302
def request(verb, path, opts = {}, &block)
225303
configure_authentication!(opts)
226304
@client.request(verb, path, opts, &block)

lib/oauth2/authenticator.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def initialize(id, secret, mode)
1717

1818
# Apply the request credentials used to authenticate to the Authorization Server
1919
#
20-
# Depending on configuration, this might be as request params or as an
20+
# Depending on the configuration, this might be as request params or as an
2121
# Authorization header.
2222
#
2323
# User-provided params and header take precedence.

0 commit comments

Comments
 (0)