oauth2 1.4.7 → 2.0.20

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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +811 -76
  4. data/CITATION.cff +20 -0
  5. data/CODE_OF_CONDUCT.md +24 -23
  6. data/CONTRIBUTING.md +221 -0
  7. data/FUNDING.md +74 -0
  8. data/IRP.md +107 -0
  9. data/{LICENSE → LICENSE.txt} +2 -2
  10. data/OIDC.md +167 -0
  11. data/README.md +1468 -166
  12. data/REEK +2 -0
  13. data/RUBOCOP.md +71 -0
  14. data/SECURITY.md +24 -0
  15. data/THREAT_MODEL.md +94 -0
  16. data/lib/oauth2/access_token.rb +276 -40
  17. data/lib/oauth2/auth_sanitizer.rb +36 -0
  18. data/lib/oauth2/authenticator.rb +51 -10
  19. data/lib/oauth2/client.rb +444 -124
  20. data/lib/oauth2/error.rb +63 -24
  21. data/lib/oauth2/filtered_attributes.rb +10 -0
  22. data/lib/oauth2/response.rb +138 -43
  23. data/lib/oauth2/strategy/assertion.rb +71 -41
  24. data/lib/oauth2/strategy/auth_code.rb +28 -5
  25. data/lib/oauth2/strategy/base.rb +2 -0
  26. data/lib/oauth2/strategy/client_credentials.rb +6 -4
  27. data/lib/oauth2/strategy/implicit.rb +20 -3
  28. data/lib/oauth2/strategy/password.rb +17 -5
  29. data/lib/oauth2/version.rb +2 -59
  30. data/lib/oauth2.rb +103 -12
  31. data/sig/oauth2/access_token.rbs +25 -0
  32. data/sig/oauth2/authenticator.rbs +22 -0
  33. data/sig/oauth2/client.rbs +52 -0
  34. data/sig/oauth2/error.rbs +8 -0
  35. data/sig/oauth2/filtered_attributes.rbs +11 -0
  36. data/sig/oauth2/response.rbs +18 -0
  37. data/sig/oauth2/sanitized_logger.rbs +32 -0
  38. data/sig/oauth2/strategy.rbs +34 -0
  39. data/sig/oauth2/thing_filter.rbs +10 -0
  40. data/sig/oauth2/version.rbs +5 -0
  41. data/sig/oauth2.rbs +9 -0
  42. data.tar.gz.sig +0 -0
  43. metadata +293 -102
  44. metadata.gz.sig +4 -0
  45. data/lib/oauth2/mac_token.rb +0 -130
  46. data/spec/helper.rb +0 -37
  47. data/spec/oauth2/access_token_spec.rb +0 -216
  48. data/spec/oauth2/authenticator_spec.rb +0 -84
  49. data/spec/oauth2/client_spec.rb +0 -506
  50. data/spec/oauth2/mac_token_spec.rb +0 -117
  51. data/spec/oauth2/response_spec.rb +0 -90
  52. data/spec/oauth2/strategy/assertion_spec.rb +0 -58
  53. data/spec/oauth2/strategy/auth_code_spec.rb +0 -107
  54. data/spec/oauth2/strategy/base_spec.rb +0 -5
  55. data/spec/oauth2/strategy/client_credentials_spec.rb +0 -69
  56. data/spec/oauth2/strategy/implicit_spec.rb +0 -26
  57. data/spec/oauth2/strategy/password_spec.rb +0 -55
  58. data/spec/oauth2/version_spec.rb +0 -23
data/REEK ADDED
@@ -0,0 +1,2 @@
1
+ ./reek: 1: ./reek:: not found
2
+ ./reek: 2: ./reek:: not found
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,24 @@
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
+ More detailed explanation of the process is in [IRP.md][IRP].
16
+
17
+ ## Additional Support
18
+
19
+ If you are interested in support for versions older than the latest release,
20
+ please consider sponsoring the project / maintainer @ https://liberapay.com/pboling/donate,
21
+ or find other sponsorship links in the [README].
22
+
23
+ [README]: README.md
24
+ [IRP]: IRP.md
data/THREAT_MODEL.md ADDED
@@ -0,0 +1,94 @@
1
+ # Threat Model Outline for oauth2 Ruby Gem
2
+
3
+ ## 1. Overview
4
+ This document outlines the threat model for the `oauth2` Ruby gem, which implements OAuth 2.0, 2.1, and OIDC Core protocols. The gem is used to facilitate secure authorization and authentication in Ruby applications.
5
+
6
+ ## 2. Assets to Protect
7
+ - OAuth access tokens, refresh tokens, and ID tokens
8
+ - User credentials (if handled)
9
+ - Client secrets and application credentials
10
+ - Sensitive user data accessed via OAuth
11
+ - Private keys and certificates (for signing/verifying tokens)
12
+
13
+ ## 3. Potential Threat Actors
14
+ - External attackers (internet-based)
15
+ - Malicious OAuth clients or resource servers
16
+ - Insiders (developers, maintainers)
17
+ - Compromised dependencies
18
+
19
+ ## 4. Attack Surfaces
20
+ - OAuth endpoints (authorization, token, revocation, introspection)
21
+ - HTTP request/response handling
22
+ - Token storage and management
23
+ - Configuration files and environment variables
24
+ - Dependency supply chain
25
+
26
+ ## 5. Threats and Mitigations
27
+
28
+ ### 5.1 Token Leakage
29
+ - **Threat:** Tokens exposed via logs, URLs, or insecure storage
30
+ - **Mitigations:**
31
+ - Avoid logging sensitive tokens
32
+ - Use secure storage mechanisms
33
+ - Never expose tokens in URLs
34
+
35
+ ### 5.2 Token Replay and Forgery
36
+ - **Threat:** Attackers reuse or forge tokens
37
+ - **Mitigations:**
38
+ - Validate token signatures and claims
39
+ - Use short-lived tokens and refresh tokens
40
+ - Implement token revocation
41
+
42
+ ### 5.3 Insecure Communication
43
+ - **Threat:** Data intercepted via MITM attacks
44
+ - **Mitigations:**
45
+ - Enforce HTTPS for all communications
46
+ - Validate SSL/TLS certificates
47
+
48
+ ### 5.4 Client Secret Exposure
49
+ - **Threat:** Client secrets leaked in code or version control
50
+ - **Mitigations:**
51
+ - Store secrets in environment variables or secure vaults
52
+ - Never commit secrets to source control
53
+
54
+ ### 5.5 Dependency Vulnerabilities
55
+ - **Threat:** Vulnerabilities in third-party libraries
56
+ - **Mitigations:**
57
+ - Regularly update dependencies
58
+ - Use tools like `bundler-audit` for vulnerability scanning
59
+
60
+ ### 5.6 Improper Input Validation
61
+ - **Threat:** Injection attacks via untrusted input
62
+ - **Mitigations:**
63
+ - Validate and sanitize all inputs
64
+ - Use parameterized queries and safe APIs
65
+
66
+ ### 5.7 Request-Target Trust Boundary Expansion
67
+ - **Threat:** Applications may pass untrusted or insufficiently validated absolute URLs into request paths that can carry OAuth credentials or authenticated state.
68
+ - **Risk:** This can expand trust boundaries, contributing to token leakage, authenticated requests to unintended hosts, or SSRF-like pivoting in the surrounding application.
69
+ - **Mitigations:**
70
+ - Prefer relative paths where practical
71
+ - Do not pass untrusted absolute URLs into token-bearing clients
72
+ - Validate or allowlist outbound request targets at the application layer
73
+ - Treat request-target validation as a separate concern from log redaction and token storage
74
+
75
+ ### 5.8 Insufficient Logging and Monitoring
76
+ - **Threat:** Attacks go undetected
77
+ - **Mitigations:**
78
+ - Log security-relevant events (without sensitive data)
79
+ - Monitor for suspicious activity
80
+
81
+ ## 6. Assumptions
82
+ - The gem is used in a secure environment with up-to-date Ruby and dependencies
83
+ - End-users are responsible for secure configuration and deployment
84
+
85
+ ## 7. Out of Scope
86
+ - Security of external OAuth providers
87
+ - Application-level business logic
88
+
89
+ ## 8. References
90
+ - [OAuth 2.0 Threat Model and Security Considerations (RFC 6819)](https://tools.ietf.org/html/rfc6819)
91
+ - [OWASP Top Ten](https://owasp.org/www-project-top-ten/)
92
+
93
+ ---
94
+ This outline should be reviewed and updated regularly as the project evolves.
@@ -1,57 +1,179 @@
1
+ # frozen_string_literal: true
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
+
1
18
  module OAuth2
2
- class AccessToken
3
- attr_reader :client, :token, :expires_in, :expires_at, :params
4
- attr_accessor :options, :refresh_token
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
5
29
 
6
- # Should these methods be deprecated?
7
30
  class << self
8
31
  # Initializes an AccessToken from a Hash
9
32
  #
10
- # @param [Client] the OAuth2::Client instance
11
- # @param [Hash] a hash of AccessToken property values
12
- # @return [AccessToken] the initalized AccessToken
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)
13
57
  def from_hash(client, hash)
14
- hash = hash.dup
15
- new(client, hash.delete('access_token') || hash.delete(:access_token), hash)
58
+ fresh = hash.dup
59
+ # If token_name is present, then use that key name
60
+ if fresh.key?(:token_name)
61
+ t_key = fresh[:token_name]
62
+ no_tokens_warning(fresh, t_key)
63
+ else
64
+ # Otherwise, if one of the supported default keys is present, use whichever has precedence
65
+ supported_keys = TOKEN_KEY_LOOKUP & fresh.keys
66
+ t_key = supported_keys[0]
67
+ extra_tokens_warning(supported_keys, t_key)
68
+ end
69
+ # :nocov:
70
+ # TODO: Get rid of this branching logic when dropping Hashie < v3.2
71
+ token = if !defined?(Hashie::VERSION) # i.e. <= "1.1.0"; the first Hashie to ship with a VERSION constant
72
+ warn("snaky_hash and oauth2 will drop support for Hashie v0 in the next major version. Please upgrade to a modern Hashie.")
73
+ # There is a bug in Hashie v0, which is accounts for.
74
+ fresh.delete(t_key) || fresh[t_key] || ""
75
+ else
76
+ fresh.delete(t_key) || ""
77
+ end
78
+ # :nocov:
79
+ new(client, token, fresh)
16
80
  end
17
81
 
18
82
  # Initializes an AccessToken from a key/value application/x-www-form-urlencoded string
19
83
  #
20
84
  # @param [Client] client the OAuth2::Client instance
21
85
  # @param [String] kvform the application/x-www-form-urlencoded string
22
- # @return [AccessToken] the initalized AccessToken
86
+ # @return [AccessToken] the initialized AccessToken
23
87
  def from_kvform(client, kvform)
24
88
  from_hash(client, Rack::Utils.parse_query(kvform))
25
89
  end
90
+
91
+ private
92
+
93
+ # Having too many is sus, and may lead to bugs. Having none is fine (e.g. refresh flow doesn't need a token).
94
+ def extra_tokens_warning(supported_keys, key)
95
+ return if OAuth2.config.silence_extra_tokens_warning
96
+ return if supported_keys.length <= 1
97
+
98
+ warn("OAuth2::AccessToken.from_hash: `hash` contained more than one 'token' key (#{supported_keys}); using #{key.inspect}.")
99
+ end
100
+
101
+ def no_tokens_warning(hash, key)
102
+ return if OAuth2.config.silence_no_tokens_warning
103
+ return if key && hash.key?(key)
104
+
105
+ warn(%[
106
+ OAuth2::AccessToken#from_hash key mismatch.
107
+ Custom token_name (#{key}) is not found in (#{hash.keys})
108
+ You may need to set `snaky: false`. See inline documentation for more info.
109
+ ])
110
+ end
26
111
  end
27
112
 
28
- # Initalize an AccessToken
113
+ # Initialize an AccessToken
114
+ #
115
+ # @note For "soon-to-expire"/"clock-skew" functionality see the `:expires_latency` option.
116
+ # @note If no token is provided, the AccessToken will be considered invalid.
117
+ # This is to prevent the possibility of a token being accidentally
118
+ # created with no token value.
119
+ # If you want to create an AccessToken with no token value,
120
+ # you can pass in an empty string or nil for the token value.
121
+ # If you want to create an AccessToken with no token value and
122
+ # no refresh token, you can pass in an empty string or nil for the
123
+ # token value and nil for the refresh token, and `raise_errors: false`.
29
124
  #
30
125
  # @param [Client] client the OAuth2::Client instance
31
- # @param [String] token the Access Token value
126
+ # @param [String] token the Access Token value (optional, may not be used in refresh flows)
32
127
  # @param [Hash] opts the options to create the Access Token with
33
128
  # @option opts [String] :refresh_token (nil) the refresh_token value
34
129
  # @option opts [FixNum, String] :expires_in (nil) the number of seconds in which the AccessToken will expire
35
130
  # @option opts [FixNum, String] :expires_at (nil) the epoch time in seconds in which AccessToken will expire
36
- # @option opts [Symbol] :mode (:header) the transmission mode of the Access Token parameter value
37
- # one of :header, :body or :query
131
+ # @option opts [FixNum, String] :expires_latency (nil) the number of seconds by which AccessToken validity will be reduced to offset latency, @version 2.0+
132
+ # @option opts [Symbol, Hash, or callable] :mode (:header) the transmission mode of the Access Token parameter value:
133
+ # either one of :header, :body or :query; or a Hash with verb symbols as keys mapping to one of these symbols
134
+ # (e.g., `{get: :query, post: :header, delete: :header}`); or a callable that accepts a request-verb parameter
135
+ # and returns one of these three symbols.
38
136
  # @option opts [String] :header_format ('Bearer %s') the string format to use for the Authorization header
137
+ #
138
+ # @example Verb-dependent Hash mode
139
+ # # Send token in query for GET, in header for POST/DELETE, in body for PUT/PATCH
140
+ # OAuth2::AccessToken.new(client, token, mode: {get: :query, post: :header, delete: :header, put: :body, patch: :body})
39
141
  # @option opts [String] :param_name ('access_token') the parameter name to use for transmission of the
40
142
  # Access Token value in :body or :query transmission mode
41
- def initialize(client, token, opts = {}) # rubocop:disable Metrics/AbcSize
143
+ # @option opts [String] :token_name (nil) the name of the response parameter that identifies the access token
144
+ # When nil one of TOKEN_KEY_LOOKUP will be used
145
+ def initialize(client, token, opts = {})
42
146
  @client = client
43
147
  @token = token.to_s
44
148
  opts = opts.dup
45
- [:refresh_token, :expires_in, :expires_at].each do |arg|
149
+ %i[refresh_token expires_in expires_at expires_latency].each do |arg|
46
150
  instance_variable_set("@#{arg}", opts.delete(arg) || opts.delete(arg.to_s))
47
151
  end
48
- @expires_in ||= opts.delete('expires')
152
+ no_tokens = (@token.nil? || @token.empty?) && (@refresh_token.nil? || @refresh_token.empty?)
153
+ if no_tokens
154
+ if @client.options[:raise_errors]
155
+ raise Error.new({
156
+ error: "OAuth2::AccessToken has no token",
157
+ error_description: "Options are: #{opts.inspect}",
158
+ })
159
+ elsif !OAuth2.config.silence_no_tokens_warning
160
+ warn("OAuth2::AccessToken has no token")
161
+ end
162
+ end
163
+ # @option opts [Fixnum, String] :expires is deprecated
164
+ @expires_in ||= opts.delete("expires")
49
165
  @expires_in &&= @expires_in.to_i
50
166
  @expires_at &&= convert_expires_at(@expires_at)
51
- @expires_at ||= Time.now.to_i + @expires_in if @expires_in
52
- @options = {:mode => opts.delete(:mode) || :header,
53
- :header_format => opts.delete(:header_format) || 'Bearer %s',
54
- :param_name => opts.delete(:param_name) || 'access_token'}
167
+ @expires_latency &&= @expires_latency.to_i
168
+ @expires_at ||= Time.now.to_i + @expires_in if @expires_in && !@expires_in.zero?
169
+ @expires_at -= @expires_latency if @expires_latency
170
+ @options = {
171
+ mode: opts.delete(:mode) || :header,
172
+ header_format: opts.delete(:header_format) || "Bearer %s",
173
+ param_name: opts.delete(:param_name) || "access_token",
174
+ }
175
+ @options[:token_name] = opts.delete(:token_name) if opts.key?(:token_name)
176
+
55
177
  @params = opts
56
178
  end
57
179
 
@@ -62,40 +184,131 @@ module OAuth2
62
184
  @params[key]
63
185
  end
64
186
 
65
- # Whether or not the token expires
187
+ # Whether the token expires
66
188
  #
67
189
  # @return [Boolean]
68
190
  def expires?
69
191
  !!@expires_at
70
192
  end
71
193
 
72
- # Whether or not the token is expired
194
+ # Check if token is expired
73
195
  #
74
- # @return [Boolean]
196
+ # @return [Boolean] true if the token is expired, false otherwise
75
197
  def expired?
76
- expires? && (expires_at < Time.now.to_i)
198
+ expires? && (expires_at <= Time.now.to_i)
77
199
  end
78
200
 
79
201
  # Refreshes the current Access Token
80
202
  #
81
- # @return [AccessToken] a new AccessToken
82
- # @note options should be carried over to the new AccessToken
83
- def refresh!(params = {})
84
- raise('A refresh_token is not available') unless refresh_token
203
+ # @param [Hash] params additional params to pass to the refresh token request
204
+ # @param [Hash] access_token_opts options that will be passed to the AccessToken initialization
205
+ #
206
+ # @yield [opts] The block to modify the refresh token request options
207
+ # @yieldparam [Hash] opts The options hash that can be modified
208
+ #
209
+ # @return [OAuth2::AccessToken] a new AccessToken instance
210
+ #
211
+ # @note current token's options are carried over to the new AccessToken
212
+ def refresh(params = {}, access_token_opts = {}, &block)
213
+ raise OAuth2::Error.new({error: "A refresh_token is not available"}) unless refresh_token
85
214
 
86
- params[:grant_type] = 'refresh_token'
215
+ params[:grant_type] = "refresh_token"
87
216
  params[:refresh_token] = refresh_token
88
- new_token = @client.get_token(params)
217
+ new_token = @client.get_token(params, access_token_opts, &block)
89
218
  new_token.options = options
90
- new_token.refresh_token = refresh_token unless new_token.refresh_token
219
+ if new_token.refresh_token
220
+ # Keep it if there is one
221
+ else
222
+ new_token.refresh_token = refresh_token
223
+ end
91
224
  new_token
92
225
  end
226
+ # A compatibility alias
227
+ # @note does not modify the receiver, so bang is not the default method
228
+ alias_method :refresh!, :refresh
229
+
230
+ # Revokes the token at the authorization server
231
+ #
232
+ # @param [Hash] params additional parameters to be sent during revocation
233
+ # @option params [String, Symbol, nil] :token_type_hint ('access_token' or 'refresh_token') hint about which token to revoke
234
+ # @option params [Symbol] :token_method (:post_with_query_string) overrides OAuth2::Client#options[:token_method]
235
+ #
236
+ # @yield [req] The block is passed the request being made, allowing customization
237
+ # @yieldparam [Faraday::Request] req The request object that can be modified
238
+ #
239
+ # @return [OAuth2::Response] OAuth2::Response instance
240
+ #
241
+ # @api public
242
+ #
243
+ # @raise [OAuth2::Error] if token_type_hint is invalid or the specified token is not available
244
+ #
245
+ # @note If the token passed to the request
246
+ # is an access token, the server MAY revoke the respective refresh
247
+ # token as well.
248
+ # @note If the token passed to the request
249
+ # is a refresh token and the authorization server supports the
250
+ # revocation of access tokens, then the authorization server SHOULD
251
+ # also invalidate all access tokens based on the same authorization
252
+ # grant
253
+ # @note If the server responds with HTTP status code 503, your code must
254
+ # assume the token still exists and may retry after a reasonable delay.
255
+ # The server may include a "Retry-After" header in the response to
256
+ # indicate how long the service is expected to be unavailable to the
257
+ # requesting client.
258
+ #
259
+ # @see https://datatracker.ietf.org/doc/html/rfc7009
260
+ # @see https://datatracker.ietf.org/doc/html/rfc7009#section-2.1
261
+ def revoke(params = {}, &block)
262
+ token_type_hint_orig = params.delete(:token_type_hint)
263
+ token_type_hint = nil
264
+ revoke_token = case token_type_hint_orig
265
+ when "access_token", :access_token
266
+ token_type_hint = "access_token"
267
+ token
268
+ when "refresh_token", :refresh_token
269
+ token_type_hint = "refresh_token"
270
+ refresh_token
271
+ when nil
272
+ if token
273
+ token_type_hint = "access_token"
274
+ token
275
+ elsif refresh_token
276
+ token_type_hint = "refresh_token"
277
+ refresh_token
278
+ end
279
+ else
280
+ 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."})
281
+ end
282
+ raise OAuth2::Error.new({error: "#{token_type_hint || "unknown token type"} is not available for revoking"}) unless revoke_token && !revoke_token.empty?
283
+
284
+ @client.revoke_token(revoke_token, token_type_hint, params, &block)
285
+ end
286
+ # A compatibility alias
287
+ # @note does not modify the receiver, so bang is not the default method
288
+ alias_method :revoke!, :revoke
93
289
 
94
290
  # Convert AccessToken to a hash which can be used to rebuild itself with AccessToken.from_hash
95
291
  #
292
+ # @note Don't return expires_latency because it has already been deducted from expires_at
293
+ #
96
294
  # @return [Hash] a hash of AccessToken property values
97
295
  def to_hash
98
- params.merge(:access_token => token, :refresh_token => refresh_token, :expires_at => expires_at)
296
+ hsh = {
297
+ access_token: token,
298
+ refresh_token: refresh_token,
299
+ expires_at: expires_at,
300
+ mode: options[:mode],
301
+ header_format: options[:header_format],
302
+ param_name: options[:param_name],
303
+ }
304
+ hsh[:token_name] = options[:token_name] if options.key?(:token_name)
305
+ # TODO: Switch when dropping Ruby < 2.5 support
306
+ # params.transform_keys(&:to_sym) # Ruby 2.5 only
307
+ # Old Ruby transform_keys alternative:
308
+ sheesh = @params.each_with_object({}) { |(k, v), memo|
309
+ memo[k.to_sym] = v
310
+ }
311
+ sheesh.merge(hsh)
99
312
  end
100
313
 
101
314
  # Make a request with the Access Token
@@ -103,9 +316,18 @@ module OAuth2
103
316
  # @param [Symbol] verb the HTTP request method
104
317
  # @param [String] path the HTTP URL path of the request
105
318
  # @param [Hash] opts the options to make the request with
106
- # @see Client#request
319
+ # @option opts [Hash] :params additional URL parameters
320
+ # @option opts [Hash, String] :body the request body
321
+ # @option opts [Hash] :headers request headers
322
+ #
323
+ # @yield [req] The block to modify the request
324
+ # @yieldparam [Faraday::Request] req The request object that can be modified
325
+ #
326
+ # @return [OAuth2::Response] the response from the request
327
+ #
328
+ # @see OAuth2::Client#request
107
329
  def request(verb, path, opts = {}, &block)
108
- configure_authentication!(opts)
330
+ configure_authentication!(opts, verb)
109
331
  @client.request(verb, path, opts, &block)
110
332
  end
111
333
 
@@ -146,17 +368,31 @@ module OAuth2
146
368
 
147
369
  # Get the headers hash (includes Authorization token)
148
370
  def headers
149
- {'Authorization' => options[:header_format] % token}
371
+ {"Authorization" => options[:header_format] % token}
150
372
  end
151
373
 
152
374
  private
153
375
 
154
- def configure_authentication!(opts) # rubocop:disable Metrics/AbcSize
155
- case options[:mode]
376
+ def configure_authentication!(opts, verb)
377
+ mode_opt = options[:mode]
378
+ mode =
379
+ if mode_opt.respond_to?(:call)
380
+ mode_opt.call(verb)
381
+ elsif mode_opt.is_a?(Hash)
382
+ key = verb.to_sym
383
+ # Try symbol key first, then string key; default to :header when missing
384
+ mode_opt[key] || mode_opt[key.to_s] || :header
385
+ else
386
+ mode_opt
387
+ end
388
+
389
+ case mode
156
390
  when :header
157
391
  opts[:headers] ||= {}
158
392
  opts[:headers].merge!(headers)
159
393
  when :query
394
+ # OAuth 2.1 note: Bearer tokens in the query string are omitted from the spec due to security risks.
395
+ # Prefer the default :header mode whenever possible.
160
396
  opts[:params] ||= {}
161
397
  opts[:params][options[:param_name]] = token
162
398
  when :body
@@ -164,11 +400,11 @@ module OAuth2
164
400
  if opts[:body].is_a?(Hash)
165
401
  opts[:body][options[:param_name]] = token
166
402
  else
167
- opts[:body] << "&#{options[:param_name]}=#{token}"
403
+ opts[:body] += "&#{options[:param_name]}=#{token}"
168
404
  end
169
405
  # @todo support for multi-part (file uploads)
170
406
  else
171
- raise("invalid :mode option of #{options[:mode]}")
407
+ raise("invalid :mode option of #{mode}")
172
408
  end
173
409
  end
174
410
 
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OAuth2
4
+ AUTH_SANITIZER = begin
5
+ auth_sanitizer_requirement = Gem::Requirement.new("~> 0.1", ">= 0.1.3")
6
+ auth_sanitizer_spec = Gem.loaded_specs["auth-sanitizer"]
7
+ unless auth_sanitizer_spec && auth_sanitizer_requirement.satisfied_by?(auth_sanitizer_spec.version)
8
+ # :nocov:
9
+ auth_sanitizer_spec = Gem::Specification.find_by_name("auth-sanitizer", auth_sanitizer_requirement)
10
+ # :nocov:
11
+ end
12
+
13
+ auth_sanitizer_loader_path = File.join(
14
+ auth_sanitizer_spec.full_gem_path,
15
+ "lib/auth_sanitizer/loader.rb",
16
+ )
17
+ unless File.file?(auth_sanitizer_loader_path)
18
+ # :nocov:
19
+ raise LoadError, "oauth2 requires auth-sanitizer #{auth_sanitizer_requirement}; " \
20
+ "loader not found at #{auth_sanitizer_loader_path}"
21
+ # :nocov:
22
+ end
23
+
24
+ auth_sanitizer_loader_namespace = Module.new
25
+ auth_sanitizer_loader_namespace.module_eval(
26
+ File.read(auth_sanitizer_loader_path),
27
+ auth_sanitizer_loader_path,
28
+ 1,
29
+ )
30
+
31
+ auth_sanitizer_loader_namespace.
32
+ const_get(:AuthSanitizer).
33
+ const_get(:Loader).
34
+ load_isolated
35
+ end
36
+ end