From 3c5fffc871ebff6b805071d7fb3160203bc55aed Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Fri, 16 May 2025 06:04:01 +0700 Subject: [PATCH 1/9] =?UTF-8?q?=F0=9F=90=9B=20Make=20OAuth2.config=20attr?= =?UTF-8?q?=5Freader?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - the hash it contains can still be modified --- lib/oauth2.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/oauth2.rb b/lib/oauth2.rb index 368e83ea..86baef76 100644 --- a/lib/oauth2.rb +++ b/lib/oauth2.rb @@ -32,7 +32,7 @@ module OAuth2 ) @config = DEFAULT_CONFIG.dup class << self - attr_accessor :config + attr_reader :config end def configure yield @config From f5885de98edbebb90da24b1864483c4c8c3ac3ad Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Fri, 16 May 2025 06:07:05 +0700 Subject: [PATCH 2/9] =?UTF-8?q?=F0=9F=90=9B=20OAuth2::AccessToken=20Errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - All errors raised are now OAuth2::Error - Improved error metadata - Improved inline documentation --- lib/oauth2/access_token.rb | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/oauth2/access_token.rb b/lib/oauth2/access_token.rb index 658971e5..da63e5e9 100644 --- a/lib/oauth2/access_token.rb +++ b/lib/oauth2/access_token.rb @@ -34,7 +34,7 @@ class << self # @note If no token keys are present, a warning will be issued unless # OAuth2.config.silence_no_tokens_warning is true # @note For "soon-to-expire"/"clock-skew" functionality see the `:expires_latency` option. - # @mote If snaky key conversion is being used, token_name needs to match the converted key. + # @note If snaky key conversion is being used, token_name needs to match the converted key. # # @example # hash = { 'access_token' => 'token_value', 'refresh_token' => 'refresh_value' } @@ -125,8 +125,10 @@ def initialize(client, token, opts = {}) no_tokens = (@token.nil? || @token.empty?) && (@refresh_token.nil? || @refresh_token.empty?) if no_tokens if @client.options[:raise_errors] - error = Error.new(opts) - raise(error) + raise Error.new({ + error: "OAuth2::AccessToken has no token", + error_description: "Options are: #{opts.inspect}", + }) elsif !OAuth2.config.silence_no_tokens_warning warn("OAuth2::AccessToken has no token") end @@ -155,14 +157,14 @@ def [](key) @params[key] end - # Whether or not the token expires + # Whether the token expires # # @return [Boolean] def expires? !!@expires_at end - # Whether or not the token is expired + # Whether the token is expired # # @return [Boolean] def expired? @@ -181,7 +183,7 @@ def refresh(params = {}, access_token_opts = {}) new_token = @client.get_token(params, access_token_opts) new_token.options = options if new_token.refresh_token - # Keep it, if there is one + # Keep it if there is one else new_token.refresh_token = refresh_token end From 16d415c9707df6879e6f1e1b6b3d1f7774360428 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Fri, 16 May 2025 06:11:19 +0700 Subject: [PATCH 3/9] =?UTF-8?q?=E2=9C=A8=20OAuth2::AccessToken#refresh=20?= =?UTF-8?q?=20supports=20block=20param?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/oauth2/access_token.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/oauth2/access_token.rb b/lib/oauth2/access_token.rb index da63e5e9..3c8a3fbf 100644 --- a/lib/oauth2/access_token.rb +++ b/lib/oauth2/access_token.rb @@ -175,12 +175,12 @@ def expired? # # @return [AccessToken] a new AccessToken # @note options should be carried over to the new AccessToken - def refresh(params = {}, access_token_opts = {}) - raise("A refresh_token is not available") unless refresh_token + def refresh(params = {}, access_token_opts = {}, &block) + raise OAuth2::Error.new({error: "A refresh_token is not available"}) unless refresh_token params[:grant_type] = "refresh_token" params[:refresh_token] = refresh_token - new_token = @client.get_token(params, access_token_opts) + new_token = @client.get_token(params, access_token_opts, &block) new_token.options = options if new_token.refresh_token # Keep it if there is one From 0e5c824873e75d8283e4d99c8cb01cbb71e8c169 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Fri, 16 May 2025 06:13:24 +0700 Subject: [PATCH 4/9] =?UTF-8?q?=F0=9F=93=9D=20Documentation=20updates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/oauth2/authenticator.rb | 2 +- lib/oauth2/client.rb | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/oauth2/authenticator.rb b/lib/oauth2/authenticator.rb index 512d1cd7..120553d3 100644 --- a/lib/oauth2/authenticator.rb +++ b/lib/oauth2/authenticator.rb @@ -17,7 +17,7 @@ def initialize(id, secret, mode) # Apply the request credentials used to authenticate to the Authorization Server # - # Depending on configuration, this might be as request params or as an + # Depending on the configuration, this might be as request params or as an # Authorization header. # # User-provided params and header take precedence. diff --git a/lib/oauth2/client.rb b/lib/oauth2/client.rb index 430200d0..04c9fc60 100644 --- a/lib/oauth2/client.rb +++ b/lib/oauth2/client.rb @@ -43,6 +43,7 @@ class Client # rubocop:disable Metrics/ClassLength # @option options [Logger] :logger (::Logger.new($stdout)) Logger instance for HTTP request/response output; requires OAUTH_DEBUG to be true # @option options [Class] :access_token_class (AccessToken) class to use for access tokens; you can subclass OAuth2::AccessToken, @version 2.0+ # @option options [Hash] :ssl SSL options for Faraday + # # @yield [builder] The Faraday connection builder def initialize(client_id, client_secret, options = {}, &block) opts = options.dup @@ -106,6 +107,7 @@ def token_url(params = nil) # Makes a request relative to the specified site root. # Updated HTTP 1.1 specification (IETF RFC 7231) relaxed the original constraint (IETF RFC 2616), # allowing the use of relative URLs in Location headers. + # # @see https://datatracker.ietf.org/doc/html/rfc7231#section-7.1.2 # # @param [Symbol] verb one of :get, :post, :put, :delete @@ -141,7 +143,7 @@ def request(verb, url, opts = {}, &block) raise(error, "Got #{response.status} status code, but no Location header was present") end when 200..299, 300..399 - # on non-redirecting 3xx statuses, just return the response + # on non-redirecting 3xx statuses, return the response response when 400..599 if opts.fetch(:raise_errors, options[:raise_errors]) @@ -164,6 +166,7 @@ def request(verb, url, opts = {}, &block) # * params can include a 'snaky' key to control snake_case conversion (default: false) # @param [Hash] access_token_opts options that will be passed to the AccessToken initialization # @param [Proc] extract_access_token (deprecated) a proc that can extract the access token from the response + # # @yield [opts] The block is passed the options being used to make the request # @yieldparam [Hash] opts options being passed to the http library # @@ -218,7 +221,8 @@ def get_token(params, access_token_opts = {}, extract_access_token = nil, &block end # The HTTP Method of the request - # @return [Symbol] HTTP verb, one of :get, :post, :put, :delete + # + # @return [Symbol] HTTP verb, one of [:get, :post, :put, :delete] def http_method http_meth = options[:token_method].to_sym return :post if http_meth == :post_with_query_string @@ -264,7 +268,7 @@ def assertion # requesting authorization. If it is provided at authorization time it MUST # also be provided with the token exchange request. # - # Providing the :redirect_uri to the OAuth2::Client instantiation will take + # Providing :redirect_uri to the OAuth2::Client instantiation will take # care of managing this. # # @api semipublic @@ -273,6 +277,7 @@ def assertion # @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3 # @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.2.1 # @see https://datatracker.ietf.org/doc/html/rfc6749#section-10.6 + # # @return [Hash] the params to add to a request or URL def redirection_params if options[:redirect_uri] @@ -309,7 +314,7 @@ def parse_snaky_params_headers(params) parse = params.key?(:parse) ? params.delete(:parse) : Response::DEFAULT_OPTIONS[:parse] snaky = params.key?(:snaky) ? params.delete(:snaky) : Response::DEFAULT_OPTIONS[:snaky] params = authenticator.apply(params) - # authenticator may add :headers, and we remove them here + # authenticator may add :headers, and we separate them from params here headers = params.delete(:headers) || {} [parse, snaky, params, headers] end From 90aad81ec4b5bf8ef019c1d25f12cd091e3bda34 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Fri, 16 May 2025 06:14:50 +0700 Subject: [PATCH 5/9] =?UTF-8?q?=E2=9C=A8=20IETF=20RFC=207009=20Token=20Rev?= =?UTF-8?q?ocation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `OAuth2::AccessToken.revoke` - `OAuth2::Client.revoke_token` --- .rubocop_gradual.lock | 51 +++++----- CHANGELOG.md | 2 + lib/oauth2/access_token.rb | 60 ++++++++++++ lib/oauth2/client.rb | 162 ++++++++++++++++++++++--------- spec/oauth2/access_token_spec.rb | 138 +++++++++++++++++++++++++- spec/oauth2/client_spec.rb | 75 +++++++++++++- 6 files changed, 406 insertions(+), 82 deletions(-) diff --git a/.rubocop_gradual.lock b/.rubocop_gradual.lock index 408ea1dc..205c040c 100644 --- a/.rubocop_gradual.lock +++ b/.rubocop_gradual.lock @@ -3,15 +3,14 @@ [66, 5, 20, "ThreadSafety/ClassInstanceVariable: Avoid class instance variables.", 2485198147], [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] ], - "lib/oauth2.rb:1956148869": [ - [35, 5, 21, "ThreadSafety/ClassAndModuleAttributes: Avoid mutating class and module attributes.", 622027168], + "lib/oauth2.rb:4176768025": [ [38, 11, 7, "ThreadSafety/ClassInstanceVariable: Avoid class instance variables.", 651502127] ], - "lib/oauth2/access_token.rb:2233632404": [ + "lib/oauth2/access_token.rb:569882683": [ [49, 13, 5, "Style/IdenticalConditionalBranches: Move `t_key` out of the conditional.", 183811513], [55, 13, 5, "Style/IdenticalConditionalBranches: Move `t_key` out of the conditional.", 183811513] ], - "lib/oauth2/authenticator.rb:3711266135": [ + "lib/oauth2/authenticator.rb:63639854": [ [42, 5, 113, "Style/ClassMethodsDefinitions: Use `class << self` to define a class method.", 734523108] ], "lib/oauth2/filtered_attributes.rb:1202323815": [ @@ -32,11 +31,11 @@ [130, 3, 52, "Gemspec/DependencyVersion: Dependency version specification is required.", 3163430777], [131, 3, 48, "Gemspec/DependencyVersion: Dependency version specification is required.", 425065368] ], - "spec/oauth2/access_token_spec.rb:3473606468": [ + "spec/oauth2/access_token_spec.rb:3105694173": [ [3, 1, 34, "RSpec/SpecFilePathFormat: Spec path should end with `o_auth2/access_token*_spec.rb`.", 1972107547], - [780, 13, 25, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 770233088], - [850, 9, 101, "Style/ClassMethodsDefinitions: Use `class << self` to define a class method.", 3022740639], - [854, 9, 79, "Style/ClassMethodsDefinitions: Use `class << self` to define a class method.", 2507338967] + [781, 13, 25, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 770233088], + [851, 9, 101, "Style/ClassMethodsDefinitions: Use `class << self` to define a class method.", 3022740639], + [855, 9, 79, "Style/ClassMethodsDefinitions: Use `class << self` to define a class method.", 2507338967] ], "spec/oauth2/authenticator_spec.rb:853320290": [ [3, 1, 36, "RSpec/SpecFilePathFormat: Spec path should end with `o_auth2/authenticator*_spec.rb`.", 819808017], @@ -45,26 +44,24 @@ [69, 15, 38, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 1480816240], [79, 13, 23, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 2314399065] ], - "spec/oauth2/client_spec.rb:2085440011": [ + "spec/oauth2/client_spec.rb:1326196445": [ [6, 1, 29, "RSpec/SpecFilePathFormat: Spec path should end with `o_auth2/client*_spec.rb`.", 439549885], - [174, 7, 492, "RSpec/NoExpectationExample: No expectation found in this example.", 1272021224], - [193, 7, 592, "RSpec/NoExpectationExample: No expectation found in this example.", 3428877205], - [206, 15, 20, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 2320605227], - [221, 15, 20, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 1276531672], - [236, 15, 43, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 1383956904], - [251, 15, 43, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 3376202107], - [829, 5, 360, "RSpec/NoExpectationExample: No expectation found in this example.", 536201463], - [838, 5, 461, "RSpec/NoExpectationExample: No expectation found in this example.", 3392600621], - [849, 5, 340, "RSpec/NoExpectationExample: No expectation found in this example.", 244592251], - [894, 63, 2, "RSpec/BeEq: Prefer `be` over `eq`.", 5860785], - [939, 11, 99, "Style/ClassMethodsDefinitions: Use `class << self` to define a class method.", 3084776886], - [943, 11, 82, "Style/ClassMethodsDefinitions: Use `class << self` to define a class method.", 1524553529], - [951, 7, 89, "RSpec/NoExpectationExample: No expectation found in this example.", 4609419], - [1039, 11, 99, "Style/ClassMethodsDefinitions: Use `class << self` to define a class method.", 3084776886], - [1043, 11, 82, "Style/ClassMethodsDefinitions: Use `class << self` to define a class method.", 1524553529], - [1123, 17, 12, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 664794325], - [1148, 5, 459, "RSpec/NoExpectationExample: No expectation found in this example.", 2216851076], - [1158, 7, 450, "RSpec/NoExpectationExample: No expectation found in this example.", 2619808549] + [175, 7, 492, "RSpec/NoExpectationExample: No expectation found in this example.", 1272021224], + [194, 7, 592, "RSpec/NoExpectationExample: No expectation found in this example.", 3428877205], + [207, 15, 20, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 2320605227], + [222, 15, 20, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 1276531672], + [237, 15, 43, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 1383956904], + [252, 15, 43, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 3376202107], + [830, 5, 360, "RSpec/NoExpectationExample: No expectation found in this example.", 536201463], + [839, 5, 461, "RSpec/NoExpectationExample: No expectation found in this example.", 3392600621], + [850, 5, 340, "RSpec/NoExpectationExample: No expectation found in this example.", 244592251], + [895, 63, 2, "RSpec/BeEq: Prefer `be` over `eq`.", 5860785], + [940, 11, 99, "Style/ClassMethodsDefinitions: Use `class << self` to define a class method.", 3084776886], + [944, 11, 82, "Style/ClassMethodsDefinitions: Use `class << self` to define a class method.", 1524553529], + [952, 7, 89, "RSpec/NoExpectationExample: No expectation found in this example.", 4609419], + [1040, 11, 99, "Style/ClassMethodsDefinitions: Use `class << self` to define a class method.", 3084776886], + [1044, 11, 82, "Style/ClassMethodsDefinitions: Use `class << self` to define a class method.", 1524553529], + [1124, 17, 12, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 664794325] ], "spec/oauth2/error_spec.rb:1209122273": [ [23, 1, 28, "RSpec/SpecFilePathFormat: Spec path should end with `o_auth2/error*_spec.rb`.", 3385870076], diff --git a/CHANGELOG.md b/CHANGELOG.md index d883f69e..eed9d95e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,8 @@ and this project adheres to [Semantic Versioning v2](https://semver.org/spec/v2. - Specify the parameter name that identifies the access token - [!645](https://gitlab.com/oauth-xx/oauth2/-/merge_requests/645) - Add `OAuth2::OAUTH_DEBUG` constant, based on `ENV["OAUTH_DEBUG"] (@pboling) - [!646](https://gitlab.com/oauth-xx/oauth2/-/merge_requests/646) - Add `OAuth2.config.silence_extra_tokens_warning`, default: false (@pboling) +- Added IETF RFC 7009 Token Revocation compliant `OAuth2::Client#revoke_token` and `OAuth2::AccessToken#revoke` + - See: https://datatracker.ietf.org/doc/html/rfc7009 ### Changed - Default value of `OAuth2.config.silence_extra_tokens_warning` was `false`, now `true` - Gem releases are now cryptographically signed, with a 20-year cert (@pboling) diff --git a/lib/oauth2/access_token.rb b/lib/oauth2/access_token.rb index 3c8a3fbf..662e1a82 100644 --- a/lib/oauth2/access_token.rb +++ b/lib/oauth2/access_token.rb @@ -193,6 +193,66 @@ def refresh(params = {}, access_token_opts = {}, &block) # @note does not modify the receiver, so bang is not the default method alias_method :refresh!, :refresh + # Revokes the token at the authorization server + # + # @param [Hash] params additional parameters to be sent during revocation + # @option params [String, Symbol, nil] :token_type_hint ('access_token' or 'refresh_token') hint about which token to revoke + # @option params [Symbol] :token_method (:post_with_query_string) overrides OAuth2::Client#options[:token_method] + # + # @yield [req] The block is passed the request being made, allowing customization + # @yieldparam [Faraday::Request] req The request object that can be modified + # + # @return [OAuth2::Response] OAuth2::Response instance + # + # @api public + # + # @raise [OAuth2::Error] if token_type_hint is invalid or the specified token is not available + # + # @note If the token passed to the request + # is an access token, the server MAY revoke the respective refresh + # token as well. + # @note If the token passed to the request + # is a refresh token and the authorization server supports the + # revocation of access tokens, then the authorization server SHOULD + # also invalidate all access tokens based on the same authorization + # grant + # @note If the server responds with HTTP status code 503, your code must + # assume the token still exists and may retry after a reasonable delay. + # The server may include a "Retry-After" header in the response to + # indicate how long the service is expected to be unavailable to the + # requesting client. + # + # @see https://datatracker.ietf.org/doc/html/rfc7009 + # @see https://datatracker.ietf.org/doc/html/rfc7009#section-2.1 + def revoke(params = {}, &block) + token_type_hint_orig = params.delete(:token_type_hint) + token_type_hint = nil + revoke_token = case token_type_hint_orig + when "access_token", :access_token + token_type_hint = "access_token" + token + when "refresh_token", :refresh_token + token_type_hint = "refresh_token" + refresh_token + when nil + if token + token_type_hint = "access_token" + token + elsif refresh_token + token_type_hint = "refresh_token" + refresh_token + end + else + 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."}) + end + raise OAuth2::Error.new({error: "#{token_type_hint || "unknown token type"} is not available for revoking"}) unless revoke_token && !revoke_token.empty? + + @client.revoke_token(revoke_token, token_type_hint, params, &block) + end + # A compatibility alias + # @note does not modify the receiver, so bang is not the default method + alias_method :revoke!, :revoke + # Convert AccessToken to a hash which can be used to rebuild itself with AccessToken.from_hash # # @note Don't return expires_latency because it has already been deducted from expires_at diff --git a/lib/oauth2/client.rb b/lib/oauth2/client.rb index 04c9fc60..16e877d3 100644 --- a/lib/oauth2/client.rb +++ b/lib/oauth2/client.rb @@ -18,7 +18,8 @@ module OAuth2 # The OAuth2::Client class class Client # rubocop:disable Metrics/ClassLength - RESERVED_PARAM_KEYS = %w[body headers params parse snaky].freeze + RESERVED_REQ_KEYS = %w[body headers params redirect_count].freeze + RESERVED_PARAM_KEYS = (RESERVED_REQ_KEYS + %w[parse snaky token_method]).freeze include FilteredAttributes @@ -34,6 +35,7 @@ class Client # rubocop:disable Metrics/ClassLength # @param [Hash] options the options to configure the client # @option options [String] :site the OAuth2 provider site host # @option options [String] :authorize_url ('/oauth/authorize') absolute or relative URL path to the Authorization endpoint + # @option options [String] :revoke_url ('/oauth/revoke') absolute or relative URL path to the Revoke endpoint # @option options [String] :token_url ('/oauth/token') absolute or relative URL path to the Token endpoint # @option options [Symbol] :token_method (:post) HTTP method to use to request token (:get, :post, :post_with_query_string) # @option options [Symbol] :auth_scheme (:basic_auth) the authentication scheme (:basic_auth, :request_body, :tls_client_auth, :private_key_jwt) @@ -54,6 +56,7 @@ def initialize(client_id, client_secret, options = {}, &block) warn("OAuth2::Client#initialize argument `extract_access_token` will be removed in oauth2 v3. Refactor to use `access_token_class`.") if opts[:extract_access_token] @options = { authorize_url: "oauth/authorize", + revoke_url: "oauth/revoke", token_url: "oauth/token", token_method: :post, auth_scheme: :basic_auth, @@ -104,6 +107,13 @@ def token_url(params = nil) connection.build_url(options[:token_url], params).to_s end + # The revoke endpoint URL of the OAuth2 provider + # + # @param [Hash] params additional query parameters + def revoke_url(params = nil) + connection.build_url(options[:revoke_url], params).to_s + end + # Makes a request relative to the specified site root. # Updated HTTP 1.1 specification (IETF RFC 7231) relaxed the original constraint (IETF RFC 2616), # allowing the use of relative URLs in Location headers. @@ -113,40 +123,42 @@ def token_url(params = nil) # @param [Symbol] verb one of :get, :post, :put, :delete # @param [String] url URL path of request # @param [Hash] opts the options to make the request with - # @option opts [Hash] :params additional query parameters for the URL of the request - # @option opts [Hash, String] :body the body of the request - # @option opts [Hash] :headers http request headers - # @option opts [Boolean] :raise_errors whether to raise an OAuth2::Error on 400+ status + # @option req_opts [Hash] :params additional query parameters for the URL of the request + # @option req_opts [Hash, String] :body the body of the request + # @option req_opts [Hash] :headers http request headers + # @option req_opts [Boolean] :raise_errors whether to raise an OAuth2::Error on 400+ status # code response for this request. Overrides the client instance setting. - # @option opts [Symbol] :parse @see Response::initialize - # @option opts [true, false] :snaky (true) @see Response::initialize + # @option req_opts [Symbol] :parse @see Response::initialize + # @option req_opts [true, false] :snaky (true) @see Response::initialize + # # @yield [req] @see Faraday::Connection#run_request - def request(verb, url, opts = {}, &block) - response = execute_request(verb, url, opts, &block) + def request(verb, url, req_opts = {}, &block) + response = execute_request(verb, url, req_opts, &block) + status = response.status - case response.status + case status when 301, 302, 303, 307 - opts[:redirect_count] ||= 0 - opts[:redirect_count] += 1 - return response if opts[:redirect_count] > options[:max_redirects] + req_opts[:redirect_count] ||= 0 + req_opts[:redirect_count] += 1 + return response if req_opts[:redirect_count] > options[:max_redirects] - if response.status == 303 + if status == 303 verb = :get - opts.delete(:body) + req_opts.delete(:body) end location = response.headers["location"] if location full_location = response.response.env.url.merge(location) - request(verb, full_location, opts) + request(verb, full_location, req_opts) else error = Error.new(response) - raise(error, "Got #{response.status} status code, but no Location header was present") + raise(error, "Got #{status} status code, but no Location header was present") end when 200..299, 300..399 # on non-redirecting 3xx statuses, return the response response when 400..599 - if opts.fetch(:raise_errors, options[:raise_errors]) + if req_opts.fetch(:raise_errors, options[:raise_errors]) error = Error.new(response) raise(error) end @@ -154,7 +166,7 @@ def request(verb, url, opts = {}, &block) response else error = Error.new(response) - raise(error, "Unhandled status code value of #{response.status}") + raise(error, "Unhandled status code value of #{status}") end end @@ -185,30 +197,8 @@ def request(verb, url, opts = {}, &block) def get_token(params, access_token_opts = {}, extract_access_token = nil, &block) warn("OAuth2::Client#get_token argument `extract_access_token` will be removed in oauth2 v3. Refactor to use `access_token_class` on #initialize.") if extract_access_token extract_access_token ||= options[:extract_access_token] - parse, snaky, params, headers = parse_snaky_params_headers(params) - - request_opts = { - raise_errors: options[:raise_errors], - parse: parse, - snaky: snaky, - } - if options[:token_method] == :post - - # NOTE: If proliferation of request types continues, we should implement a parser solution for Request, - # just like we have with Response. - request_opts[:body] = if headers["Content-Type"] == "application/json" - params.to_json - else - params - end - - request_opts[:headers] = {"Content-Type" => "application/x-www-form-urlencoded"} - else - request_opts[:params] = params - request_opts[:headers] = {} - end - request_opts[:headers].merge!(headers) - response = request(http_method, token_url, request_opts, &block) + req_opts = params_to_req_opts(params) + response = request(http_method, token_url, req_opts, &block) # In v1.4.x, the deprecated extract_access_token option retrieves the token from the response. # We preserve this behavior here, but a custom access_token_class that implements #from_hash @@ -220,6 +210,49 @@ def get_token(params, access_token_opts = {}, extract_access_token = nil, &block end end + # Makes a request to revoke a token at the authorization server + # + # @param [String] token The token to be revoked + # @param [String, nil] token_type_hint A hint about the type of the token being revoked (e.g., 'access_token' or 'refresh_token') + # @param [Hash] params additional parameters for the token revocation + # @option params [Symbol] :parse (:automatic) parsing strategy for the response + # @option params [Boolean] :snaky (true) whether to convert response keys to snake_case + # @option params [Symbol] :token_method (:post_with_query_string) overrides OAuth2::Client#options[:token_method] + # @option params [Hash] :headers Additional request headers + # + # @yield [req] The block is passed the request being made, allowing customization + # @yieldparam [Faraday::Request] req The request object that can be modified + # + # @return [OAuth2::Response] OAuth2::Response instance + # + # @api public + # + # @note If the token passed to the request + # is an access token, the server MAY revoke the respective refresh + # token as well. + # @note If the token passed to the request + # is a refresh token and the authorization server supports the + # revocation of access tokens, then the authorization server SHOULD + # also invalidate all access tokens based on the same authorization + # grant + # @note If the server responds with HTTP status code 503, your code must + # assume the token still exists and may retry after a reasonable delay. + # The server may include a "Retry-After" header in the response to + # indicate how long the service is expected to be unavailable to the + # requesting client. + # + # @see https://datatracker.ietf.org/doc/html/rfc7009 + # @see https://datatracker.ietf.org/doc/html/rfc7009#section-2.1 + def revoke_token(token, token_type_hint = nil, params = {}, &block) + params[:token_method] ||= :post_with_query_string + req_opts = params_to_req_opts(params) + req_opts[:params] ||= {} + req_opts[:params][:token] = token + req_opts[:params][:token_type_hint] = token_type_hint if token_type_hint + + request(http_method, revoke_url, req_opts, &block) + end + # The HTTP Method of the request # # @return [Symbol] HTTP verb, one of [:get, :post, :put, :delete] @@ -289,6 +322,33 @@ def redirection_params private + # A generic token request options parser + def params_to_req_opts(params) + parse, snaky, token_method, params, headers = parse_snaky_params_headers(params) + req_opts = { + raise_errors: options[:raise_errors], + token_method: token_method || options[:token_method], + parse: parse, + snaky: snaky, + } + if req_opts[:token_method] == :post + # NOTE: If proliferation of request types continues, we should implement a parser solution for Request, + # just like we have with Response. + req_opts[:body] = if headers["Content-Type"] == "application/json" + params.to_json + else + params + end + + req_opts[:headers] = {"Content-Type" => "application/x-www-form-urlencoded"} + else + req_opts[:params] = params + req_opts[:headers] = {} + end + req_opts[:headers].merge!(headers) + req_opts + end + # Processes and transforms the input parameters for OAuth requests # # @param [Hash] params the input parameters to process @@ -299,6 +359,7 @@ def redirection_params # @return [Array<(Symbol, Boolean, Hash, Hash)>] Returns an array containing: # - [Symbol, nil] parse strategy # - [Boolean] snaky flag for response key transformation + # - [Symbol, nil] token_method overrides options[:token_method] for a request # - [Hash] processed parameters # - [Hash] HTTP headers # @@ -313,10 +374,11 @@ def parse_snaky_params_headers(params) end.to_h parse = params.key?(:parse) ? params.delete(:parse) : Response::DEFAULT_OPTIONS[:parse] snaky = params.key?(:snaky) ? params.delete(:snaky) : Response::DEFAULT_OPTIONS[:snaky] + token_method = params.delete(:token_method) if params.key?(:token_method) params = authenticator.apply(params) # authenticator may add :headers, and we separate them from params here headers = params.delete(:headers) || {} - [parse, snaky, params, headers] + [parse, snaky, token_method, params, headers] end # Executes an HTTP request with error handling and response processing @@ -341,10 +403,14 @@ def parse_snaky_params_headers(params) # @api private def execute_request(verb, url, opts = {}) url = connection.build_url(url).to_s + # See: Hash#partition https://bugs.ruby-lang.org/issues/16252 + req_opts, oauth_opts = opts. + partition { |k, _v| RESERVED_REQ_KEYS.include?(k.to_s) }. + map { |p| Hash[p] } begin - response = connection.run_request(verb, url, opts[:body], opts[:headers]) do |req| - req.params.update(opts[:params]) if opts[:params] + response = connection.run_request(verb, url, req_opts[:body], req_opts[:headers]) do |req| + req.params.update(req_opts[:params]) if req_opts[:params] yield(req) if block_given? end rescue Faraday::ConnectionFailed => e @@ -353,8 +419,8 @@ def execute_request(verb, url, opts = {}) raise TimeoutError, e end - parse = opts.key?(:parse) ? opts.delete(:parse) : Response::DEFAULT_OPTIONS[:parse] - snaky = opts.key?(:snaky) ? opts.delete(:snaky) : Response::DEFAULT_OPTIONS[:snaky] + parse = oauth_opts.key?(:parse) ? oauth_opts.delete(:parse) : Response::DEFAULT_OPTIONS[:parse] + snaky = oauth_opts.key?(:snaky) ? oauth_opts.delete(:snaky) : Response::DEFAULT_OPTIONS[:snaky] Response.new(response, parse: parse, snaky: snaky) end diff --git a/spec/oauth2/access_token_spec.rb b/spec/oauth2/access_token_spec.rb index 59a44fb0..5603dba8 100644 --- a/spec/oauth2/access_token_spec.rb +++ b/spec/oauth2/access_token_spec.rb @@ -19,6 +19,7 @@ stub.send(verb, "/token/body") { |env| [200, {}, env[:body]] } end stub.post("/oauth/token") { |_env| [200, {"Content-Type" => "application/json"}, refresh_body] } + stub.post("/oauth/revoke") { |env| [200, {"Content-type" => "application/json"}, env[:body]] } end end end @@ -388,7 +389,7 @@ def assert_initialized_token(target) let(:token) { "" } it "raises on initialize" do - block_is_expected.to raise_error(OAuth2::Error, {mode: :this_is_bad, raise_errors: true}.to_s) + block_is_expected.to raise_error(OAuth2::Error, {error: "OAuth2::AccessToken has no token", error_description: "Options are: {mode: :this_is_bad, raise_errors: true}"}.to_s) end end @@ -396,7 +397,7 @@ def assert_initialized_token(target) let(:token) { nil } it "raises on initialize" do - block_is_expected.to raise_error(OAuth2::Error, {mode: :this_is_bad, raise_errors: true}.to_s) + block_is_expected.to raise_error(OAuth2::Error, {error: "OAuth2::AccessToken has no token", error_description: "Options are: {mode: :this_is_bad, raise_errors: true}"}.to_s) end end end @@ -602,7 +603,7 @@ def assert_initialized_token(target) context "when there is no refresh_token" do it "raises on initialize" do - block_is_expected.to raise_error(OAuth2::Error, {raise_errors: true}.to_s) + block_is_expected.to raise_error(OAuth2::Error, {error: "OAuth2::AccessToken has no token", error_description: "Options are: {raise_errors: true}"}.to_s) end end @@ -628,7 +629,7 @@ def assert_initialized_token(target) context "when there is no refresh_token" do it "raises on initialize" do - block_is_expected.to raise_error(OAuth2::Error, {raise_errors: true}.to_s) + block_is_expected.to raise_error(OAuth2::Error, {error: "OAuth2::AccessToken has no token", error_description: "Options are: {raise_errors: true}"}.to_s) end end @@ -874,7 +875,7 @@ def self.contains_token?(hash) end it "raises when no refresh_token" do - block_is_expected.to raise_error("A refresh_token is not available") + block_is_expected.to raise_error(OAuth2::Error, {error: "A refresh_token is not available"}.to_s) end end @@ -932,6 +933,133 @@ def self.contains_token?(hash) end end + describe "#revoke" do + let(:token) { "monkey123" } + let(:refresh_token) { "refreshmonkey123" } + let(:access_token) { described_class.new(client, token, refresh_token: refresh_token) } + + context "with no token_type_hint specified" do + it "revokes the access token by default" do + expect(access_token.revoke.status).to eq(200) + end + end + + context "with access_token token_type_hint" do + it "revokes the access token" do + expect { + access_token.revoke(token_type_hint: "access_token") + }.not_to raise_error + end + end + + context "with refresh_token token_type_hint" do + it "revokes the refresh token" do + expect { + access_token.revoke(token_type_hint: "refresh_token") + }.not_to raise_error + end + end + + context "with invalid token_type_hint" do + it "raises an OAuth2::Error" do + expect { + access_token.revoke(token_type_hint: "invalid_type") + }.to raise_error(OAuth2::Error, /token_type_hint must be one of/) + end + end + + context "when refresh_token is specified but not available" do + let(:access_token) { described_class.new(client, "abc", refresh_token: nil) } + + it "raises an OAuth2::Error" do + expect { + access_token.revoke(token_type_hint: "refresh_token") + }.to raise_error(OAuth2::Error, /refresh_token is not available for revoking/) + end + end + + context "when refresh_token is, but access_token is not, available" do + let(:access_token) { described_class.new(client, "abc", refresh_token: refresh_token) } + + before do + allow(client).to receive(:revoke_token). + with(refresh_token, "refresh_token", {}). + and_return(OAuth2::Response.new(double(status: 200))) + # The code path being tested shouldn't be reachable... so this is hacky. + # Testing it for anal level compliance. Revoking a refresh token without an access token is valid. + # In other words, the implementation of AccessToken doesn't allow instantiation without an access token. + # But in a revocation scenario it should theoretically work. + # It is intended that AccessToken be subclassed, so this is worth testing, as subclasses may change behavior. + allow(access_token).to receive(:token).and_return(nil) + end + + it "revokes refresh_token" do + expect { + access_token.revoke + }.not_to raise_error + end + end + + context "when no tokens are available" do + let(:access_token) { described_class.new(client, "abc", refresh_token: nil) } + + before do + # The code path being tested shouldn't be reachable... so this is hacky. + # Testing it for anal level compliance. Revoking a refresh token without an access token is valid. + # In other words, the implementation of AccessToken doesn't allow instantiation without an access token. + # But in a revocation scenario it should theoretically work. + # It is intended that AccessToken be subclassed, so this is worth testing, as subclasses may change behavior. + allow(access_token).to receive(:token).and_return(nil) + end + + it "raises an OAuth2::Error" do + expect { + access_token.revoke + }.to raise_error(OAuth2::Error, /unknown token type is not available for revoking/) + end + end + + context "with additional params" do + before do + allow(client).to receive(:revoke_token). + with(token, "access_token", {extra: "param"}). + and_return(OAuth2::Response.new(double(status: 200))) + end + + it "passes them to the client" do + expect { + access_token.revoke({extra: "param"}) + }.not_to raise_error + end + end + + context "with a block" do + it "passes the block to the client" do + expect { + access_token.revoke do |_req| + puts "Hello from the other side" + end + }.not_to raise_error + end + + it "has status 200" do + expect( + access_token.revoke do |_req| + puts "Hello again" + end.status, + ).to eq(200) + end + + it "executes the block" do + @apple = 0 + access_token.revoke do |_req| + @apple += 1 + end + expect(@apple).to eq(1) + end + end + end + describe "#to_hash" do it "return a hash equal to the hash used to initialize access token" do hash = { diff --git a/spec/oauth2/client_spec.rb b/spec/oauth2/client_spec.rb index f683e498..49163e0e 100644 --- a/spec/oauth2/client_spec.rb +++ b/spec/oauth2/client_spec.rb @@ -21,6 +21,7 @@ stub.get("/different_encoding") { |_env| [500, {"Content-Type" => "application/json"}, NKF.nkf("-We", JSON.dump(error: error_value, error_description: "∞"))] } stub.get("/ascii_8bit_encoding") { |_env| [500, {"Content-Type" => "application/json"}, JSON.dump(error: "invalid_request", error_description: "é").force_encoding("ASCII-8BIT")] } stub.get("/unhandled_status") { |_env| [600, {}, nil] } + stub.post("/oauth/revoke") { |env| [200, {"Content-type" => "application/json"}, env[:body]] } end end end @@ -1151,7 +1152,9 @@ def self.contains_token?(hash) [200, {"Content-Type" => "application/json"}, JSON.dump("access_token" => "the-token")] end end - client.get_token({"arbitrary" => "parameter"}) # rubocop:disable Style/BracesAroundHashParameters + expect { + client.get_token({"arbitrary" => "parameter"}) # rubocop:disable Style/BracesAroundHashParameters + }.not_to raise_error end context "when token_method is set to post_with_query_string" do @@ -1161,7 +1164,9 @@ def self.contains_token?(hash) [200, {"Content-Type" => "application/json"}, JSON.dump("access_token" => "the-token")] end end - client.get_token({"state" => "abc123"}) # rubocop:disable Style/BracesAroundHashParameters + expect { + client.get_token({"state" => "abc123"}) # rubocop:disable Style/BracesAroundHashParameters + }.not_to raise_error end end @@ -1173,6 +1178,72 @@ def stubbed_client(params = {}, &stubs) end end + describe "#revoke_token" do + let(:token) { "banana-foster" } + + context "with token string" do + it "makes request with token param" do + expect { + instance.revoke_token(token) + }.not_to raise_error + end + + it "has status 200" do + expect(instance.revoke_token(token).status).to eq(200) + end + end + + context "with token_type_hint" do + it "makes request with token_type_hint param" do + expect { + instance.revoke_token(token, "access_token") + }.not_to raise_error + end + + it "has status 200" do + expect(instance.revoke_token(token, "access_token").status).to eq(200) + end + end + + context "with additional params" do + it "merges additional params" do + expect { + instance.revoke_token(token, nil, extra: "param") + }.not_to raise_error + end + + it "has status 200" do + expect(instance.revoke_token(token, nil, extra: "param").status).to eq(200) + end + end + + context "with block" do + it "passes block to request" do + expect { + instance.revoke_token(token) do |_req| + puts "Hello from the other side" + end + }.not_to raise_error + end + + it "has status 200" do + expect( + instance.revoke_token(token) do |_req| + puts "Hello there" + end.status, + ).to eq(200) + end + + it "executes block" do + @apple = 0 + instance.revoke_token(token) do |_req| + @apple += 1 + end + expect(@apple).to eq(1) + end + end + end + it "instantiates an HTTP Method with this client" do expect(subject.http_method).to be_a(Symbol) end From ee5e044a90e93812c844e55d1e67ec6ab9a8ffe8 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Fri, 16 May 2025 06:25:17 +0700 Subject: [PATCH 6/9] =?UTF-8?q?=F0=9F=92=9A=20Fix=20specs=20for=20old=20Ru?= =?UTF-8?q?by?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spec/oauth2/access_token_spec.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/oauth2/access_token_spec.rb b/spec/oauth2/access_token_spec.rb index 5603dba8..b9186888 100644 --- a/spec/oauth2/access_token_spec.rb +++ b/spec/oauth2/access_token_spec.rb @@ -389,7 +389,7 @@ def assert_initialized_token(target) let(:token) { "" } it "raises on initialize" do - block_is_expected.to raise_error(OAuth2::Error, {error: "OAuth2::AccessToken has no token", error_description: "Options are: {mode: :this_is_bad, raise_errors: true}"}.to_s) + block_is_expected.to raise_error(OAuth2::Error, {error: "OAuth2::AccessToken has no token", error_description: "Options are: #{{mode: :this_is_bad, raise_errors: true}}"}.to_s) end end @@ -397,7 +397,7 @@ def assert_initialized_token(target) let(:token) { nil } it "raises on initialize" do - block_is_expected.to raise_error(OAuth2::Error, {error: "OAuth2::AccessToken has no token", error_description: "Options are: {mode: :this_is_bad, raise_errors: true}"}.to_s) + block_is_expected.to raise_error(OAuth2::Error, {error: "OAuth2::AccessToken has no token", error_description: "Options are: #{{mode: :this_is_bad, raise_errors: true}}"}.to_s) end end end @@ -603,7 +603,7 @@ def assert_initialized_token(target) context "when there is no refresh_token" do it "raises on initialize" do - block_is_expected.to raise_error(OAuth2::Error, {error: "OAuth2::AccessToken has no token", error_description: "Options are: {raise_errors: true}"}.to_s) + block_is_expected.to raise_error(OAuth2::Error, {error: "OAuth2::AccessToken has no token", error_description: "Options are: #{{raise_errors: true}}"}.to_s) end end @@ -629,7 +629,7 @@ def assert_initialized_token(target) context "when there is no refresh_token" do it "raises on initialize" do - block_is_expected.to raise_error(OAuth2::Error, {error: "OAuth2::AccessToken has no token", error_description: "Options are: {raise_errors: true}"}.to_s) + block_is_expected.to raise_error(OAuth2::Error, {error: "OAuth2::AccessToken has no token", error_description: "Options are: #{{raise_errors: true}}"}.to_s) end end From 19d4506fda4e4425b478a9ea8032ae59e862812d Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Fri, 16 May 2025 06:33:17 +0700 Subject: [PATCH 7/9] =?UTF-8?q?=F0=9F=9A=A8=20lint=20lock=20update?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .rubocop_gradual.lock | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.rubocop_gradual.lock b/.rubocop_gradual.lock index 205c040c..d1b2d34a 100644 --- a/.rubocop_gradual.lock +++ b/.rubocop_gradual.lock @@ -31,8 +31,12 @@ [130, 3, 52, "Gemspec/DependencyVersion: Dependency version specification is required.", 3163430777], [131, 3, 48, "Gemspec/DependencyVersion: Dependency version specification is required.", 425065368] ], - "spec/oauth2/access_token_spec.rb:3105694173": [ + "spec/oauth2/access_token_spec.rb:443932125": [ [3, 1, 34, "RSpec/SpecFilePathFormat: Spec path should end with `o_auth2/access_token*_spec.rb`.", 1972107547], + [392, 142, 40, "Lint/LiteralInInterpolation: Literal interpolation detected.", 4210228387], + [400, 142, 40, "Lint/LiteralInInterpolation: Literal interpolation detected.", 4210228387], + [606, 142, 20, "Lint/LiteralInInterpolation: Literal interpolation detected.", 304063511], + [632, 142, 20, "Lint/LiteralInInterpolation: Literal interpolation detected.", 304063511], [781, 13, 25, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 770233088], [851, 9, 101, "Style/ClassMethodsDefinitions: Use `class << self` to define a class method.", 3022740639], [855, 9, 79, "Style/ClassMethodsDefinitions: Use `class << self` to define a class method.", 2507338967] From f204c7be92d27533f810fd0c806290e4ff3492ad Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Fri, 16 May 2025 06:37:45 +0700 Subject: [PATCH 8/9] =?UTF-8?q?=F0=9F=93=9D=20Documentation=20updates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/oauth2/client.rb | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/oauth2/client.rb b/lib/oauth2/client.rb index 16e877d3..25cfada7 100644 --- a/lib/oauth2/client.rb +++ b/lib/oauth2/client.rb @@ -291,6 +291,15 @@ def client_credentials @client_credentials ||= OAuth2::Strategy::ClientCredentials.new(self) end + # The Assertion strategy + # + # This allows for assertion-based authentication where an identity provider + # asserts the identity of the user or client application seeking access. + # + # @see http://datatracker.ietf.org/doc/html/rfc7521 + # @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-assertions-01#section-4.1 + # + # @return [OAuth2::Strategy::Assertion] the initialized Assertion strategy def assertion @assertion ||= OAuth2::Strategy::Assertion.new(self) end @@ -523,4 +532,4 @@ def oauth_debug_logging(builder) builder.response(:logger, options[:logger], bodies: true) if OAuth2::OAUTH_DEBUG end end -end +end \ No newline at end of file From 20a5f2b59d7fc7cfbef6752bc69974c98d9bc702 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Fri, 16 May 2025 06:54:55 +0700 Subject: [PATCH 9/9] =?UTF-8?q?=F0=9F=93=9D=20Documentation=20updates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .rubocop_gradual.lock | 2 +- lib/oauth2/access_token.rb | 26 +++++++++++++++++++++----- lib/oauth2/client.rb | 25 ++++++++++++++++++------- 3 files changed, 40 insertions(+), 13 deletions(-) diff --git a/.rubocop_gradual.lock b/.rubocop_gradual.lock index d1b2d34a..db206cd4 100644 --- a/.rubocop_gradual.lock +++ b/.rubocop_gradual.lock @@ -6,7 +6,7 @@ "lib/oauth2.rb:4176768025": [ [38, 11, 7, "ThreadSafety/ClassInstanceVariable: Avoid class instance variables.", 651502127] ], - "lib/oauth2/access_token.rb:569882683": [ + "lib/oauth2/access_token.rb:3471244990": [ [49, 13, 5, "Style/IdenticalConditionalBranches: Move `t_key` out of the conditional.", 183811513], [55, 13, 5, "Style/IdenticalConditionalBranches: Move `t_key` out of the conditional.", 183811513] ], diff --git a/lib/oauth2/access_token.rb b/lib/oauth2/access_token.rb index 662e1a82..da7e6987 100644 --- a/lib/oauth2/access_token.rb +++ b/lib/oauth2/access_token.rb @@ -164,17 +164,24 @@ def expires? !!@expires_at end - # Whether the token is expired + # Check if token is expired # - # @return [Boolean] + # @return [Boolean] true if the token is expired, false otherwise def expired? expires? && (expires_at <= Time.now.to_i) end # Refreshes the current Access Token # - # @return [AccessToken] a new AccessToken - # @note options should be carried over to the new AccessToken + # @param [Hash] params additional params to pass to the refresh token request + # @param [Hash] access_token_opts options that will be passed to the AccessToken initialization + # + # @yield [opts] The block to modify the refresh token request options + # @yieldparam [Hash] opts The options hash that can be modified + # + # @return [OAuth2::AccessToken] a new AccessToken instance + # + # @note current token's options are carried over to the new AccessToken def refresh(params = {}, access_token_opts = {}, &block) raise OAuth2::Error.new({error: "A refresh_token is not available"}) unless refresh_token @@ -282,7 +289,16 @@ def to_hash # @param [Symbol] verb the HTTP request method # @param [String] path the HTTP URL path of the request # @param [Hash] opts the options to make the request with - # @see Client#request + # @option opts [Hash] :params additional URL parameters + # @option opts [Hash, String] :body the request body + # @option opts [Hash] :headers request headers + # + # @yield [req] The block to modify the request + # @yieldparam [Faraday::Request] req The request object that can be modified + # + # @return [OAuth2::Response] the response from the request + # + # @see OAuth2::Client#request def request(verb, path, opts = {}, &block) configure_authentication!(opts) @client.request(verb, path, opts, &block) diff --git a/lib/oauth2/client.rb b/lib/oauth2/client.rb index 25cfada7..c2ea5814 100644 --- a/lib/oauth2/client.rb +++ b/lib/oauth2/client.rb @@ -72,13 +72,16 @@ def initialize(client_id, client_secret, options = {}, &block) # Set the site host # - # @param value [String] the OAuth2 provider site host + # @param [String] value the OAuth2 provider site host + # @return [String] the site host value def site=(value) @connection = nil @site = value end # The Faraday connection object + # + # @return [Faraday::Connection] the initialized Faraday connection def connection @connection ||= Faraday.new(site, options[:connection_opts]) do |builder| @@ -95,6 +98,7 @@ def connection # The authorize endpoint URL of the OAuth2 provider # # @param [Hash] params additional query parameters + # @return [String] the constructed authorize URL def authorize_url(params = {}) params = (params || {}).merge(redirection_params) connection.build_url(options[:authorize_url], params).to_s @@ -102,25 +106,28 @@ def authorize_url(params = {}) # The token endpoint URL of the OAuth2 provider # - # @param [Hash] params additional query parameters + # @param [Hash, nil] params additional query parameters + # @return [String] the constructed token URL def token_url(params = nil) connection.build_url(options[:token_url], params).to_s end # The revoke endpoint URL of the OAuth2 provider # - # @param [Hash] params additional query parameters + # @param [Hash, nil] params additional query parameters + # @return [String] the constructed revoke URL def revoke_url(params = nil) connection.build_url(options[:revoke_url], params).to_s end # Makes a request relative to the specified site root. + # # Updated HTTP 1.1 specification (IETF RFC 7231) relaxed the original constraint (IETF RFC 2616), # allowing the use of relative URLs in Location headers. # # @see https://datatracker.ietf.org/doc/html/rfc7231#section-7.1.2 # - # @param [Symbol] verb one of :get, :post, :put, :delete + # @param [Symbol] verb one of [:get, :post, :put, :delete] # @param [String] url URL path of request # @param [Hash] opts the options to make the request with # @option req_opts [Hash] :params additional query parameters for the URL of the request @@ -129,9 +136,13 @@ def revoke_url(params = nil) # @option req_opts [Boolean] :raise_errors whether to raise an OAuth2::Error on 400+ status # code response for this request. Overrides the client instance setting. # @option req_opts [Symbol] :parse @see Response::initialize - # @option req_opts [true, false] :snaky (true) @see Response::initialize + # @option req_opts [Boolean] :snaky (true) @see Response::initialize # - # @yield [req] @see Faraday::Connection#run_request + # @yield [req] The block is passed the request being made, allowing customization + # @yieldparam [Faraday::Request] req The request object that can be modified + # @see Faraday::Connection#run_request + # + # @return [OAuth2::Response] the response from the request def request(verb, url, req_opts = {}, &block) response = execute_request(verb, url, req_opts, &block) status = response.status @@ -532,4 +543,4 @@ def oauth_debug_logging(builder) builder.response(:logger, options[:logger], bodies: true) if OAuth2::OAUTH_DEBUG end end -end \ No newline at end of file +end