oauth2 2.0.10 → 2.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,23 +5,39 @@ require "multi_xml"
5
5
  require "rack"
6
6
 
7
7
  module OAuth2
8
- # OAuth2::Response class
8
+ # The Response class handles HTTP responses in the OAuth2 gem, providing methods
9
+ # to access and parse response data in various formats.
10
+ #
11
+ # @since 1.0.0
9
12
  class Response
13
+ # Default configuration options for Response instances
14
+ #
15
+ # @return [Hash] The default options hash
10
16
  DEFAULT_OPTIONS = {
11
17
  parse: :automatic,
12
18
  snaky: true,
19
+ snaky_hash_klass: SnakyHash::StringKeyed,
13
20
  }.freeze
21
+
22
+ # @return [Faraday::Response] The raw Faraday response object
14
23
  attr_reader :response
24
+
25
+ # @return [Hash] The options hash for this instance
15
26
  attr_accessor :options
16
27
 
17
- # Procs that, when called, will parse a response body according
18
- # to the specified format.
28
+ # @private
29
+ # Storage for response body parser procedures
30
+ #
31
+ # @return [Hash<Symbol, Proc>] Hash of parser procs keyed by format symbol
19
32
  @@parsers = {
20
33
  query: ->(body) { Rack::Utils.parse_query(body) },
21
34
  text: ->(body) { body },
22
35
  }
23
36
 
24
- # Content type assignments for various potential HTTP content types.
37
+ # @private
38
+ # Maps content types to parser symbols
39
+ #
40
+ # @return [Hash<String, Symbol>] Hash of content types mapped to parser symbols
25
41
  @@content_types = {
26
42
  "application/x-www-form-urlencoded" => :query,
27
43
  "text/plain" => :text,
@@ -29,9 +45,11 @@ module OAuth2
29
45
 
30
46
  # Adds a new content type parser.
31
47
  #
32
- # @param [Symbol] key A descriptive symbol key such as :json or :query.
33
- # @param [Array] mime_types One or more mime types to which this parser applies.
34
- # @yield [String] A block returning parsed content.
48
+ # @param [Symbol] key A descriptive symbol key such as :json or :query
49
+ # @param [Array<String>, String] mime_types One or more mime types to which this parser applies
50
+ # @yield [String] Block that will be called to parse the response body
51
+ # @yieldparam [String] body The response body to parse
52
+ # @return [void]
35
53
  def self.register_parser(key, mime_types, &block)
36
54
  key = key.to_sym
37
55
  @@parsers[key] = block
@@ -43,38 +61,48 @@ module OAuth2
43
61
  # Initializes a Response instance
44
62
  #
45
63
  # @param [Faraday::Response] response The Faraday response instance
46
- # @param [Symbol] parse (:automatic) how to parse the response body. one of :query (for x-www-form-urlencoded),
47
- # :json, or :automatic (determined by Content-Type response header)
48
- # @param [true, false] snaky (true) Convert @parsed to a snake-case,
49
- # indifferent-access SnakyHash::StringKeyed, which is a subclass of Hashie::Mash (from hashie gem)?
50
- # @param [Hash] options all other options for initializing the instance
51
- def initialize(response, parse: :automatic, snaky: true, **options)
64
+ # @param [Symbol] parse (:automatic) How to parse the response body
65
+ # @param [Boolean] snaky (true) Whether to convert parsed response to snake_case using SnakyHash
66
+ # @param [Class, nil] snaky_hash_klass (nil) Custom class for snake_case hash conversion
67
+ # @param [Hash] options Additional options for the response
68
+ # @option options [Symbol] :parse (:automatic) Parse strategy (:query, :json, or :automatic)
69
+ # @option options [Boolean] :snaky (true) Enable/disable snake_case conversion
70
+ # @option options [Class] :snaky_hash_klass (SnakyHash::StringKeyed) Class to use for hash conversion
71
+ # @return [OAuth2::Response] The new Response instance
72
+ def initialize(response, parse: :automatic, snaky: true, snaky_hash_klass: nil, **options)
52
73
  @response = response
53
74
  @options = {
54
75
  parse: parse,
55
76
  snaky: snaky,
77
+ snaky_hash_klass: snaky_hash_klass,
56
78
  }.merge(options)
57
79
  end
58
80
 
59
81
  # The HTTP response headers
82
+ #
83
+ # @return [Hash] The response headers
60
84
  def headers
61
85
  response.headers
62
86
  end
63
87
 
64
88
  # The HTTP response status code
89
+ #
90
+ # @return [Integer] The response status code
65
91
  def status
66
92
  response.status
67
93
  end
68
94
 
69
95
  # The HTTP response body
96
+ #
97
+ # @return [String] The response body or empty string if nil
70
98
  def body
71
99
  response.body || ""
72
100
  end
73
101
 
74
- # The {#response} {#body} as parsed by {#parser}.
102
+ # The parsed response body
75
103
  #
76
- # @return [Object] As returned by {#parser} if it is #call-able.
77
- # @return [nil] If the {#parser} is not #call-able.
104
+ # @return [Object, SnakyHash::StringKeyed] The parsed response body
105
+ # @return [nil] If no parser is available
78
106
  def parsed
79
107
  return @parsed if defined?(@parsed)
80
108
 
@@ -91,36 +119,36 @@ module OAuth2
91
119
  end
92
120
 
93
121
  if options[:snaky] && @parsed.is_a?(Hash)
94
- parsed = SnakyHash::StringKeyed.new(@parsed)
95
- @parsed = parsed.to_h
122
+ hash_klass = options[:snaky_hash_klass] || DEFAULT_OPTIONS[:snaky_hash_klass]
123
+ @parsed = hash_klass[@parsed]
96
124
  end
97
125
 
98
126
  @parsed
99
127
  end
100
128
 
101
- # Attempts to determine the content type of the response.
129
+ # Determines the content type of the response
130
+ #
131
+ # @return [String, nil] The content type or nil if headers are not present
102
132
  def content_type
103
133
  return unless response.headers
104
134
 
105
135
  ((response.headers.values_at("content-type", "Content-Type").compact.first || "").split(";").first || "").strip.downcase
106
136
  end
107
137
 
108
- # Determines the parser (a Proc or other Object which responds to #call)
109
- # that will be passed the {#body} (and optional {#response}) to supply
110
- # {#parsed}.
138
+ # Determines the parser to be used for the response body
111
139
  #
112
- # The parser can be supplied as the +:parse+ option in the form of a Proc
113
- # (or other Object responding to #call) or a Symbol. In the latter case,
114
- # the actual parser will be looked up in {@@parsers} by the supplied Symbol.
140
+ # @note The parser can be supplied as the +:parse+ option in the form of a Proc
141
+ # (or other Object responding to #call) or a Symbol. In the latter case,
142
+ # the actual parser will be looked up in {@@parsers} by the supplied Symbol.
115
143
  #
116
- # If no +:parse+ option is supplied, the lookup Symbol will be determined
117
- # by looking up {#content_type} in {@@content_types}.
144
+ # @note If no +:parse+ option is supplied, the lookup Symbol will be determined
145
+ # by looking up {#content_type} in {@@content_types}.
118
146
  #
119
- # If {#parser} is a Proc, it will be called with no arguments, just
120
- # {#body}, or {#body} and {#response}, depending on the Proc's arity.
147
+ # @note If {#parser} is a Proc, it will be called with no arguments, just
148
+ # {#body}, or {#body} and {#response}, depending on the Proc's arity.
121
149
  #
122
- # @return [Proc, #call] If a parser was found.
123
- # @return [nil] If no parser was found.
150
+ # @return [Proc, #call] The parser proc or callable object
151
+ # @return [nil] If no suitable parser is found
124
152
  def parser
125
153
  return @parser if defined?(@parser)
126
154
 
@@ -136,12 +164,16 @@ module OAuth2
136
164
  end
137
165
  end
138
166
 
167
+ # Register XML parser
168
+ # @api private
139
169
  OAuth2::Response.register_parser(:xml, ["text/xml", "application/rss+xml", "application/rdf+xml", "application/atom+xml", "application/xml"]) do |body|
140
170
  next body unless body.respond_to?(:to_str)
141
171
 
142
172
  MultiXml.parse(body)
143
173
  end
144
174
 
175
+ # Register JSON parser
176
+ # @api private
145
177
  OAuth2::Response.register_parser(:json, ["application/json", "text/javascript", "application/hal+json", "application/vnd.collection+json", "application/vnd.api+json", "application/problem+json"]) do |body|
146
178
  next body unless body.respond_to?(:to_str)
147
179
 
@@ -95,7 +95,10 @@ module OAuth2
95
95
  def build_assertion(claims, encoding_opts)
96
96
  raise ArgumentError.new(message: "Please provide an encoding_opts hash with :algorithm and :key") if !encoding_opts.is_a?(Hash) || (%i[algorithm key] - encoding_opts.keys).any?
97
97
 
98
- JWT.encode(claims, encoding_opts[:key], encoding_opts[:algorithm])
98
+ headers = {}
99
+ headers[:kid] = encoding_opts[:kid] if encoding_opts.key?(:kid)
100
+
101
+ JWT.encode(claims, encoding_opts[:key], encoding_opts[:algorithm], headers)
99
102
  end
100
103
  end
101
104
  end
@@ -4,6 +4,16 @@ module OAuth2
4
4
  module Strategy
5
5
  # The Authorization Code Strategy
6
6
  #
7
+ # OAuth 2.1 notes:
8
+ # - PKCE is required for all OAuth clients using the authorization code flow (especially public clients).
9
+ # This library does not enforce PKCE generation/verification; implement PKCE in your application when required.
10
+ # - Redirect URIs must be compared using exact string matching by the Authorization Server.
11
+ # This client forwards redirect_uri but does not perform server-side validation.
12
+ #
13
+ # References:
14
+ # - OAuth 2.1 draft: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-13
15
+ # - OAuth for native apps (RFC 8252) and PKCE (RFC 7636)
16
+ #
7
17
  # @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-15#section-4.1
8
18
  class AuthCode < Base
9
19
  # The required query parameters for the authorize URL
File without changes
@@ -4,6 +4,14 @@ module OAuth2
4
4
  module Strategy
5
5
  # The Implicit Strategy
6
6
  #
7
+ # IMPORTANT (OAuth 2.1): The Implicit grant (response_type=token) is omitted from the OAuth 2.1 draft specification.
8
+ # It remains here for backward compatibility with OAuth 2.0 providers. Prefer the Authorization Code flow with PKCE.
9
+ #
10
+ # References:
11
+ # - OAuth 2.1 draft: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-13
12
+ # - Why drop implicit: https://aaronparecki.com/2019/12/12/21/its-time-for-oauth-2-dot-1
13
+ # - Background: https://fusionauth.io/learn/expert-advice/oauth/differences-between-oauth-2-oauth-2-1/
14
+ #
7
15
  # @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-26#section-4.2
8
16
  class Implicit < Base
9
17
  # The required query parameters for the authorize URL
@@ -4,6 +4,14 @@ module OAuth2
4
4
  module Strategy
5
5
  # The Resource Owner Password Credentials Authorization Strategy
6
6
  #
7
+ # IMPORTANT (OAuth 2.1): The Resource Owner Password Credentials grant is omitted in OAuth 2.1.
8
+ # It remains here for backward compatibility with OAuth 2.0 providers. Prefer Authorization Code + PKCE.
9
+ #
10
+ # References:
11
+ # - OAuth 2.1 draft: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-13
12
+ # - Okta explainer: https://developer.okta.com/blog/2019/12/13/oauth-2-1-how-many-rfcs
13
+ # - FusionAuth blog: https://fusionauth.io/blog/2020/04/15/whats-new-in-oauth-2-1
14
+ #
7
15
  # @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-15#section-4.3
8
16
  class Password < Base
9
17
  # Not used for this strategy
@@ -2,6 +2,6 @@
2
2
 
3
3
  module OAuth2
4
4
  module Version
5
- VERSION = "2.0.10"
5
+ VERSION = "2.0.17"
6
6
  end
7
7
  end
data/lib/oauth2.rb CHANGED
@@ -24,22 +24,58 @@ require_relative "oauth2/access_token"
24
24
  require_relative "oauth2/response"
25
25
 
26
26
  # The namespace of this library
27
+ #
28
+ # This module is the entry point and top-level namespace for the oauth2 gem.
29
+ # It exposes configuration, constants, and requires the primary public classes.
27
30
  module OAuth2
31
+ # When true, enables verbose HTTP logging via Faraday's logger middleware.
32
+ # Controlled by the OAUTH_DEBUG environment variable. Any case-insensitive
33
+ # value equal to "true" will enable debugging.
34
+ #
35
+ # @return [Boolean]
28
36
  OAUTH_DEBUG = ENV.fetch("OAUTH_DEBUG", "false").casecmp("true").zero?
37
+
38
+ # Default configuration values for the oauth2 library.
39
+ #
40
+ # @example Toggle warnings
41
+ # OAuth2.configure do |config|
42
+ # config[:silence_extra_tokens_warning] = false
43
+ # config[:silence_no_tokens_warning] = false
44
+ # end
45
+ #
46
+ # @return [SnakyHash::SymbolKeyed] A mutable Hash-like config with symbol keys
29
47
  DEFAULT_CONFIG = SnakyHash::SymbolKeyed.new(
30
48
  silence_extra_tokens_warning: true,
31
49
  silence_no_tokens_warning: true,
32
50
  )
51
+
52
+ # The current runtime configuration for the library.
53
+ #
54
+ # @return [SnakyHash::SymbolKeyed]
33
55
  @config = DEFAULT_CONFIG.dup
56
+
34
57
  class << self
58
+ # Access the current configuration.
59
+ #
60
+ # Prefer using {OAuth2.configure} to mutate configuration.
61
+ #
62
+ # @return [SnakyHash::SymbolKeyed]
35
63
  attr_reader :config
36
64
  end
65
+
66
+ # Configure global library behavior.
67
+ #
68
+ # Yields the mutable configuration object so callers can update settings.
69
+ #
70
+ # @yieldparam [SnakyHash::SymbolKeyed] config the configuration object
71
+ # @return [void]
37
72
  def configure
38
73
  yield @config
39
74
  end
40
75
  module_function :configure
41
76
  end
42
77
 
78
+ # Extend OAuth2::Version with VersionGem helpers to provide semantic version helpers.
43
79
  OAuth2::Version.class_eval do
44
80
  extend VersionGem::Basic
45
81
  end
@@ -0,0 +1,25 @@
1
+ module OAuth2
2
+ class AccessToken
3
+ def self.from_hash: (OAuth2::Client, Hash[untyped, untyped]) -> OAuth2::AccessToken
4
+ def self.from_kvform: (OAuth2::Client, String) -> OAuth2::AccessToken
5
+
6
+ def initialize: (OAuth2::Client, String, ?Hash[Symbol, untyped]) -> void
7
+ def []: (String | Symbol) -> untyped
8
+ def expires?: () -> bool
9
+ def expired?: () -> bool
10
+ def refresh: (?Hash[untyped, untyped], ?Hash[Symbol, untyped]) { (untyped) -> void } -> OAuth2::AccessToken
11
+ def revoke: (?Hash[Symbol, untyped]) { (untyped) -> void } -> OAuth2::Response
12
+ def to_hash: () -> Hash[Symbol, untyped]
13
+ def request: (Symbol, String, ?Hash[Symbol, untyped]) { (untyped) -> void } -> OAuth2::Response
14
+ def get: (String, ?Hash[Symbol, untyped]) { (untyped) -> void } -> OAuth2::Response
15
+ def post: (String, ?Hash[Symbol, untyped]) { (untyped) -> void } -> OAuth2::Response
16
+ def put: (String, ?Hash[Symbol, untyped]) { (untyped) -> void } -> OAuth2::Response
17
+ def patch: (String, ?Hash[Symbol, untyped]) { (untyped) -> void } -> OAuth2::Response
18
+ def delete: (String, ?Hash[Symbol, untyped]) { (untyped) -> void } -> OAuth2::Response
19
+ def headers: () -> Hash[String, String]
20
+ def configure_authentication!: (Hash[Symbol, untyped], Symbol) -> void
21
+ def convert_expires_at: (untyped) -> (Time | Integer | nil)
22
+
23
+ attr_accessor response: OAuth2::Response
24
+ end
25
+ end
@@ -0,0 +1,22 @@
1
+ module OAuth2
2
+ class Authenticator
3
+ include OAuth2::FilteredAttributes
4
+
5
+ attr_reader mode: (Symbol | String)
6
+ attr_reader id: String?
7
+ attr_reader secret: String?
8
+
9
+ def initialize: (String? id, String? secret, (Symbol | String) mode) -> void
10
+
11
+ def apply: (Hash[untyped, untyped]) -> Hash[untyped, untyped]
12
+
13
+ def self.encode_basic_auth: (String, String) -> String
14
+
15
+ private
16
+
17
+ def apply_params_auth: (Hash[untyped, untyped]) -> Hash[untyped, untyped]
18
+ def apply_client_id: (Hash[untyped, untyped]) -> Hash[untyped, untyped]
19
+ def apply_basic_auth: (Hash[untyped, untyped]) -> Hash[untyped, untyped]
20
+ def basic_auth_header: () -> Hash[String, String]
21
+ end
22
+ end
@@ -0,0 +1,52 @@
1
+ module OAuth2
2
+ class Client
3
+ RESERVED_REQ_KEYS: Array[String]
4
+ RESERVED_PARAM_KEYS: Array[String]
5
+
6
+ include OAuth2::FilteredAttributes
7
+
8
+ attr_reader id: String
9
+ attr_reader secret: String
10
+ attr_reader site: String?
11
+ attr_accessor options: Hash[Symbol, untyped]
12
+ attr_writer connection: untyped
13
+
14
+ def initialize: (String client_id, String client_secret, ?Hash[Symbol, untyped]) { (untyped) -> void } -> void
15
+
16
+ def site=: (String) -> String
17
+
18
+ def connection: () -> untyped
19
+
20
+ def authorize_url: (?Hash[untyped, untyped]) -> String
21
+ def token_url: (?Hash[untyped, untyped]) -> String
22
+ def revoke_url: (?Hash[untyped, untyped]) -> String
23
+
24
+ def request: (Symbol verb, String url, ?Hash[Symbol, untyped]) { (untyped) -> void } -> OAuth2::Response
25
+
26
+ def get_token: (Hash[untyped, untyped] params, ?Hash[Symbol, untyped] access_token_opts, ?Proc) { (Hash[Symbol, untyped]) -> void } -> (OAuth2::AccessToken | nil)
27
+
28
+ def revoke_token: (String token, ?String token_type_hint, ?Hash[Symbol, untyped]) { (untyped) -> void } -> OAuth2::Response
29
+
30
+ def http_method: () -> Symbol
31
+
32
+ def auth_code: () -> OAuth2::Strategy::AuthCode
33
+ def implicit: () -> OAuth2::Strategy::Implicit
34
+ def password: () -> OAuth2::Strategy::Password
35
+ def client_credentials: () -> OAuth2::Strategy::ClientCredentials
36
+ def assertion: () -> OAuth2::Strategy::Assertion
37
+
38
+ def redirection_params: () -> Hash[String, String]
39
+
40
+ private
41
+
42
+ def params_to_req_opts: (Hash[untyped, untyped]) -> Hash[Symbol, untyped]
43
+ def parse_snaky_params_headers: (Hash[untyped, untyped]) -> [Symbol, bool, untyped, (Symbol | nil), Hash[untyped, untyped], Hash[String, String]]
44
+ def execute_request: (Symbol verb, String url, ?Hash[Symbol, untyped]) { (Faraday::Request) -> void } -> OAuth2::Response
45
+ def authenticator: () -> OAuth2::Authenticator
46
+ def parse_response_legacy: (OAuth2::Response, Hash[Symbol, untyped], Proc) -> (OAuth2::AccessToken | nil)
47
+ def parse_response: (OAuth2::Response, Hash[Symbol, untyped]) -> (OAuth2::AccessToken | nil)
48
+ def build_access_token: (OAuth2::Response, Hash[Symbol, untyped], untyped) -> OAuth2::AccessToken
49
+ def build_access_token_legacy: (OAuth2::Response, Hash[Symbol, untyped], Proc) -> (OAuth2::AccessToken | nil)
50
+ def oauth_debug_logging: (untyped) -> void
51
+ end
52
+ end
@@ -0,0 +1,8 @@
1
+ module OAuth2
2
+ class Error < StandardError
3
+ def initialize: (OAuth2::Response) -> void
4
+ def code: () -> (String | Integer | nil)
5
+ def description: () -> (String | nil)
6
+ def response: () -> OAuth2::Response
7
+ end
8
+ end
@@ -0,0 +1,6 @@
1
+ module OAuth2
2
+ module FilteredAttributes
3
+ def self.included: (untyped) -> untyped
4
+ def filtered_attributes: (*String) -> void
5
+ end
6
+ end
@@ -0,0 +1,18 @@
1
+ module OAuth2
2
+ class Response
3
+ DEFAULT_OPTIONS: Hash[Symbol, untyped]
4
+
5
+ def self.register_parser: (Symbol key, (Array[String] | String) mime_types) { (String) -> untyped } -> void
6
+
7
+ def initialize: (untyped response, parse: Symbol?, snaky: bool?, snaky_hash_klass: untyped?, options: Hash[Symbol, untyped]?) -> void
8
+ def headers: () -> Hash[untyped, untyped]
9
+ def status: () -> Integer
10
+ def body: () -> String
11
+ def parsed: () -> untyped
12
+ def content_type: () -> (String | nil)
13
+ def parser: () -> (untyped | nil)
14
+
15
+ attr_reader response: untyped
16
+ attr_accessor options: Hash[Symbol, untyped]
17
+ end
18
+ end
@@ -0,0 +1,34 @@
1
+ module OAuth2
2
+ module Strategy
3
+ class Base
4
+ def initialize: (OAuth2::Client) -> void
5
+ end
6
+
7
+ class AuthCode < Base
8
+ def authorize_params: (?Hash[untyped, untyped]) -> Hash[untyped, untyped]
9
+ def authorize_url: (?Hash[untyped, untyped]) -> String
10
+ def get_token: (String, ?Hash[untyped, untyped], ?Hash[Symbol, untyped]) -> OAuth2::AccessToken
11
+ end
12
+
13
+ class Implicit < Base
14
+ def authorize_params: (?Hash[untyped, untyped]) -> Hash[untyped, untyped]
15
+ def authorize_url: (?Hash[untyped, untyped]) -> String
16
+ def get_token: (*untyped) -> void
17
+ end
18
+
19
+ class Password < Base
20
+ def authorize_url: () -> void
21
+ def get_token: (String, String, ?Hash[untyped, untyped], ?Hash[Symbol, untyped]) -> OAuth2::AccessToken
22
+ end
23
+
24
+ class ClientCredentials < Base
25
+ def authorize_url: () -> void
26
+ def get_token: (?Hash[untyped, untyped], ?Hash[Symbol, untyped]) -> OAuth2::AccessToken
27
+ end
28
+
29
+ class Assertion < Base
30
+ def authorize_url: () -> void
31
+ def get_token: (Hash[untyped, untyped], Hash[Symbol, untyped], ?Hash[Symbol, untyped], ?Hash[Symbol, untyped]) -> OAuth2::AccessToken
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,5 @@
1
+ module OAuth2
2
+ module Version
3
+ VERSION: String
4
+ end
5
+ end
data/sig/oauth2.rbs ADDED
@@ -0,0 +1,9 @@
1
+ module OAuth2
2
+ OAUTH_DEBUG: bool
3
+
4
+ DEFAULT_CONFIG: untyped
5
+ @config: untyped
6
+
7
+ def self.config: () -> untyped
8
+ def self.configure: () { (untyped) -> void } -> void
9
+ end
data.tar.gz.sig CHANGED
Binary file