oauth2 1.4.11 → 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 (45) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +802 -85
  4. data/CITATION.cff +20 -0
  5. data/CODE_OF_CONDUCT.md +24 -23
  6. data/CONTRIBUTING.md +211 -34
  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 +1389 -290
  12. data/REEK +2 -0
  13. data/RUBOCOP.md +71 -0
  14. data/SECURITY.md +13 -15
  15. data/THREAT_MODEL.md +94 -0
  16. data/lib/oauth2/access_token.rb +273 -39
  17. data/lib/oauth2/auth_sanitizer.rb +36 -0
  18. data/lib/oauth2/authenticator.rb +48 -9
  19. data/lib/oauth2/client.rb +414 -129
  20. data/lib/oauth2/error.rb +61 -24
  21. data/lib/oauth2/filtered_attributes.rb +10 -0
  22. data/lib/oauth2/response.rb +136 -43
  23. data/lib/oauth2/strategy/assertion.rb +68 -40
  24. data/lib/oauth2/strategy/auth_code.rb +25 -4
  25. data/lib/oauth2/strategy/base.rb +0 -0
  26. data/lib/oauth2/strategy/client_credentials.rb +3 -3
  27. data/lib/oauth2/strategy/implicit.rb +17 -2
  28. data/lib/oauth2/strategy/password.rb +14 -4
  29. data/lib/oauth2/version.rb +2 -59
  30. data/lib/oauth2.rb +101 -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 +304 -83
  44. metadata.gz.sig +4 -0
  45. data/lib/oauth2/mac_token.rb +0 -130
data/lib/oauth2/error.rb CHANGED
@@ -1,42 +1,79 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OAuth2
4
+ # Represents an OAuth2 error condition.
5
+ #
6
+ # Wraps details from an OAuth2::Response or Hash payload returned by an
7
+ # authorization server, exposing error code and description per RFC 6749.
4
8
  class Error < StandardError
5
- attr_reader :response, :code, :description
9
+ # @return [OAuth2::Response, Hash, Object] Original response or payload used to build the error
10
+ # @return [String] Raw body content (if available)
11
+ # @return [String, nil] Error code (e.g., 'invalid_grant')
12
+ # @return [String, nil] Human-readable description for the error
13
+ attr_reader :response, :body, :code, :description
6
14
 
7
- # standard error values include:
8
- # :invalid_request, :invalid_client, :invalid_token, :invalid_grant, :unsupported_grant_type, :invalid_scope
15
+ # Create a new OAuth2::Error
16
+ #
17
+ # @param [OAuth2::Response, Hash, Object] response A Response or error payload
9
18
  def initialize(response)
10
- response.error = self
11
19
  @response = response
12
-
13
- if response.parsed.is_a?(Hash)
14
- @code = response.parsed['error']
15
- @description = response.parsed['error_description']
16
- error_description = "#{@code}: #{@description}"
20
+ @code = nil
21
+ @description = nil
22
+ if response.respond_to?(:parsed)
23
+ if response.parsed.is_a?(Hash)
24
+ @code = response.parsed["error"]
25
+ @description = response.parsed["error_description"]
26
+ end
27
+ elsif response.is_a?(Hash)
28
+ @code = response["error"]
29
+ @description = response["error_description"]
17
30
  end
18
-
19
- super(error_message(response.body, :error_description => error_description))
31
+ @body = if response.respond_to?(:body)
32
+ response.body
33
+ else
34
+ @response
35
+ end
36
+ message_opts = parse_error_description(@code, @description)
37
+ super(error_message(@body, message_opts))
20
38
  end
21
39
 
22
- # Makes a error message
23
- # @param [String] response_body response body of request
24
- # @param [String] opts :error_description error description to show first line
40
+ private
41
+
42
+ # Builds a multi-line error message including description and raw body.
43
+ #
44
+ # @param [String, #encode] response_body Response body content
45
+ # @param [Hash] opts Options including :error_description
46
+ # @return [String] Message suitable for StandardError
25
47
  def error_message(response_body, opts = {})
26
- message = []
48
+ lines = []
49
+
50
+ lines << opts[:error_description] if opts[:error_description]
27
51
 
28
- opts[:error_description] && (message << opts[:error_description])
52
+ error_string = if response_body.respond_to?(:encode) && opts[:error_description].respond_to?(:encoding)
53
+ script_encoding = opts[:error_description].encoding
54
+ response_body.encode(script_encoding, invalid: :replace, undef: :replace)
55
+ else
56
+ response_body
57
+ end
58
+
59
+ lines << error_string
60
+
61
+ lines.join("\n")
62
+ end
29
63
 
30
- error_message = if opts[:error_description] && opts[:error_description].respond_to?(:encoding)
31
- script_encoding = opts[:error_description].encoding
32
- response_body.encode(script_encoding, :invalid => :replace, :undef => :replace)
33
- else
34
- response_body
35
- end
64
+ # Formats the OAuth2 error code and description into a single string.
65
+ #
66
+ # @param [String, nil] code OAuth2 error code
67
+ # @param [String, nil] description OAuth2 error description
68
+ # @return [Hash] Options hash containing :error_description when present
69
+ def parse_error_description(code, description)
70
+ return {} unless code || description
36
71
 
37
- message << error_message
72
+ error_description = ""
73
+ error_description += "#{code}: " if code
74
+ error_description += description if description
38
75
 
39
- message.join("\n")
76
+ {error_description: error_description}
40
77
  end
41
78
  end
42
79
  end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OAuth2
4
+ # Permanent alias for {OAuth2::AUTH_SANITIZER::FilteredAttributes}.
5
+ #
6
+ # This constant is intentionally kept in the `OAuth2` namespace because it
7
+ # was part of the public API before the implementation was extracted into the
8
+ # `auth-sanitizer` gem. It will **not** be deprecated or removed.
9
+ FilteredAttributes = OAuth2::AUTH_SANITIZER::FilteredAttributes
10
+ end
@@ -1,93 +1,186 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'multi_json'
4
- require 'multi_xml'
5
- require 'rack'
3
+ require "json"
4
+ require "multi_xml"
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
16
+ DEFAULT_OPTIONS = {
17
+ parse: :automatic,
18
+ snaky: true,
19
+ snaky_hash_klass: SnakyHash::StringKeyed,
20
+ }.freeze
21
+
22
+ # @return [Faraday::Response] The raw Faraday response object
10
23
  attr_reader :response
11
- attr_accessor :error, :options
12
24
 
13
- # Procs that, when called, will parse a response body according
14
- # to the specified format.
25
+ # @return [Hash] The options hash for this instance
26
+ attr_accessor :options
27
+
28
+ # @private
29
+ # Storage for response body parser procedures
30
+ #
31
+ # @return [Hash<Symbol, Proc>] Hash of parser procs keyed by format symbol
15
32
  @@parsers = {
16
- :json => lambda { |body| MultiJson.load(body) rescue body }, # rubocop:disable Style/RescueModifier
17
- :query => lambda { |body| Rack::Utils.parse_query(body) },
18
- :text => lambda { |body| body },
33
+ query: ->(body) { Rack::Utils.parse_query(body) },
34
+ text: ->(body) { body },
19
35
  }
20
36
 
21
- # 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
22
41
  @@content_types = {
23
- 'application/json' => :json,
24
- 'text/javascript' => :json,
25
- 'application/x-www-form-urlencoded' => :query,
26
- 'text/plain' => :text,
42
+ "application/x-www-form-urlencoded" => :query,
43
+ "text/plain" => :text,
27
44
  }
28
45
 
29
- # Adds a new content type parser.
30
- #
31
- # @param [Symbol] key A descriptive symbol key such as :json or :query.
32
- # @param [Array] mime_types One or more mime types to which this parser applies.
33
- # @yield [String] A block returning parsed content.
34
- def self.register_parser(key, mime_types, &block)
35
- key = key.to_sym
36
- @@parsers[key] = block
37
- Array(mime_types).each do |mime_type|
38
- @@content_types[mime_type] = key
46
+ class << self
47
+ # Adds a new content type parser.
48
+ #
49
+ # @param [Symbol] key A descriptive symbol key such as :json or :query
50
+ # @param [Array<String>, String] mime_types One or more mime types to which this parser applies
51
+ # @yield [String] Block that will be called to parse the response body
52
+ # @yieldparam [String] body The response body to parse
53
+ # @return [void]
54
+ def register_parser(key, mime_types, &block)
55
+ key = key.to_sym
56
+ @@parsers[key] = block
57
+ Array(mime_types).each do |mime_type|
58
+ @@content_types[mime_type] = key
59
+ end
39
60
  end
40
61
  end
41
62
 
42
63
  # Initializes a Response instance
43
64
  #
44
65
  # @param [Faraday::Response] response The Faraday response instance
45
- # @param [Hash] opts options in which to initialize the instance
46
- # @option opts [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
- def initialize(response, opts = {})
66
+ # @param [Symbol] parse (:automatic) How to parse the response body
67
+ # @param [Boolean] snaky (true) Whether to convert parsed response to snake_case using SnakyHash
68
+ # @param [Class, nil] snaky_hash_klass (nil) Custom class for snake_case hash conversion
69
+ # @param [Hash] options Additional options for the response
70
+ # @option options [Symbol] :parse (:automatic) Parse strategy (:query, :json, or :automatic)
71
+ # @option options [Boolean] :snaky (true) Enable/disable snake_case conversion
72
+ # @option options [Class] :snaky_hash_klass (SnakyHash::StringKeyed) Class to use for hash conversion
73
+ # @return [OAuth2::Response] The new Response instance
74
+ def initialize(response, parse: :automatic, snaky: true, snaky_hash_klass: nil, **options)
49
75
  @response = response
50
- @options = {:parse => :automatic}.merge(opts)
76
+ @options = {
77
+ parse: parse,
78
+ snaky: snaky,
79
+ snaky_hash_klass: snaky_hash_klass,
80
+ }.merge(options)
51
81
  end
52
82
 
53
83
  # The HTTP response headers
84
+ #
85
+ # @return [Hash] The response headers
54
86
  def headers
55
87
  response.headers
56
88
  end
57
89
 
58
90
  # The HTTP response status code
91
+ #
92
+ # @return [Integer] The response status code
59
93
  def status
60
94
  response.status
61
95
  end
62
96
 
63
97
  # The HTTP response body
98
+ #
99
+ # @return [String] The response body or empty string if nil
64
100
  def body
65
- response.body || ''
101
+ response.body || ""
66
102
  end
67
103
 
68
- # The parsed response body.
69
- # Will attempt to parse application/x-www-form-urlencoded and
70
- # application/json Content-Type response bodies
104
+ # The parsed response body
105
+ #
106
+ # @return [Object, SnakyHash::StringKeyed] The parsed response body
107
+ # @return [nil] If no parser is available
71
108
  def parsed
72
- return nil unless @@parsers.key?(parser)
109
+ return @parsed if defined?(@parsed)
110
+
111
+ @parsed =
112
+ if parser.respond_to?(:call)
113
+ case parser.arity
114
+ when 0
115
+ parser.call
116
+ when 1
117
+ parser.call(body)
118
+ else
119
+ parser.call(body, response)
120
+ end
121
+ end
122
+
123
+ if options[:snaky] && @parsed.is_a?(Hash)
124
+ hash_klass = options[:snaky_hash_klass] || DEFAULT_OPTIONS[:snaky_hash_klass]
125
+ @parsed = hash_klass[@parsed]
126
+ end
73
127
 
74
- @parsed ||= @@parsers[parser].call(body)
128
+ @parsed
75
129
  end
76
130
 
77
- # Attempts to determine the content type of the response.
131
+ # Determines the content type of the response
132
+ #
133
+ # @return [String, nil] The content type or nil if headers are not present
78
134
  def content_type
79
- ((response.headers.values_at('content-type', 'Content-Type').compact.first || '').split(';').first || '').strip
135
+ return unless response.headers
136
+
137
+ ((response.headers.values_at("content-type", "Content-Type").compact.first || "").split(";").first || "").strip.downcase
80
138
  end
81
139
 
82
- # Determines the parser that will be used to supply the content of #parsed
140
+ # Determines the parser to be used for the response body
141
+ #
142
+ # @note The parser can be supplied as the +:parse+ option in the form of a Proc
143
+ # (or other Object responding to #call) or a Symbol. In the latter case,
144
+ # the actual parser will be looked up in {@@parsers} by the supplied Symbol.
145
+ #
146
+ # @note If no +:parse+ option is supplied, the lookup Symbol will be determined
147
+ # by looking up {#content_type} in {@@content_types}.
148
+ #
149
+ # @note If {#parser} is a Proc, it will be called with no arguments, just
150
+ # {#body}, or {#body} and {#response}, depending on the Proc's arity.
151
+ #
152
+ # @return [Proc, #call] The parser proc or callable object
153
+ # @return [nil] If no suitable parser is found
83
154
  def parser
84
- return options[:parse].to_sym if @@parsers.key?(options[:parse])
155
+ return @parser if defined?(@parser)
85
156
 
86
- @@content_types[content_type]
157
+ @parser =
158
+ if options[:parse].respond_to?(:call)
159
+ options[:parse]
160
+ elsif options[:parse]
161
+ @@parsers[options[:parse].to_sym]
162
+ end
163
+
164
+ @parser ||= @@parsers[@@content_types[content_type]]
87
165
  end
88
166
  end
89
167
  end
90
168
 
91
- OAuth2::Response.register_parser(:xml, ['text/xml', 'application/rss+xml', 'application/rdf+xml', 'application/atom+xml']) do |body|
92
- MultiXml.parse(body) rescue body # rubocop:disable Style/RescueModifier
169
+ # Register XML parser
170
+ # @api private
171
+ OAuth2::Response.register_parser(:xml, ["text/xml", "application/rss+xml", "application/rdf+xml", "application/atom+xml", "application/xml"]) do |body|
172
+ next body unless body.respond_to?(:to_str)
173
+
174
+ MultiXml.parse(body)
175
+ end
176
+
177
+ # Register JSON parser
178
+ # @api private
179
+ 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|
180
+ next body unless body.respond_to?(:to_str)
181
+
182
+ body = body.dup.force_encoding(Encoding::ASCII_8BIT) if body.respond_to?(:force_encoding)
183
+ next body if body.respond_to?(:empty?) && body.empty?
184
+
185
+ JSON.parse(body)
93
186
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'jwt'
3
+ require "jwt"
4
4
 
5
5
  module OAuth2
6
6
  module Strategy
@@ -10,15 +10,22 @@ module OAuth2
10
10
  #
11
11
  # Sample usage:
12
12
  # client = OAuth2::Client.new(client_id, client_secret,
13
- # :site => 'http://localhost:8080')
13
+ # :site => 'http://localhost:8080',
14
+ # :auth_scheme => :request_body)
14
15
  #
15
- # params = {:hmac_secret => "some secret",
16
- # # or :private_key => "private key string",
17
- # :iss => "http://localhost:3001",
18
- # :prn => "me@here.com",
19
- # :exp => Time.now.utc.to_i + 3600}
16
+ # claim_set = {
17
+ # :iss => "http://localhost:3001",
18
+ # :aud => "http://localhost:8080/oauth2/token",
19
+ # :sub => "me@example.com",
20
+ # :exp => Time.now.utc.to_i + 3600,
21
+ # }
20
22
  #
21
- # access = client.assertion.get_token(params)
23
+ # encoding = {
24
+ # :algorithm => 'HS256',
25
+ # :key => 'secret_key',
26
+ # }
27
+ #
28
+ # access = client.assertion.get_token(claim_set, encoding)
22
29
  # access.token # actual access_token string
23
30
  # access.get("/api/stuff") # making api calls with access token in header
24
31
  #
@@ -27,50 +34,71 @@ module OAuth2
27
34
  #
28
35
  # @raise [NotImplementedError]
29
36
  def authorize_url
30
- raise(NotImplementedError, 'The authorization endpoint is not used in this strategy')
37
+ raise(NotImplementedError, "The authorization endpoint is not used in this strategy")
31
38
  end
32
39
 
33
40
  # Retrieve an access token given the specified client.
34
41
  #
35
- # @param [Hash] params assertion params
36
- # pass either :hmac_secret or :private_key, but not both.
42
+ # @param [Hash] claims the hash representation of the claims that should be encoded as a JWT (JSON Web Token)
43
+ #
44
+ # For reading on JWT and claim keys:
45
+ # @see https://github.com/jwt/ruby-jwt
46
+ # @see https://datatracker.ietf.org/doc/html/rfc7519#section-4.1
47
+ # @see https://datatracker.ietf.org/doc/html/rfc7523#section-3
48
+ # @see https://www.iana.org/assignments/jwt/jwt.xhtml
49
+ #
50
+ # There are many possible claim keys, and applications may ask for their own custom keys.
51
+ # Some typically required ones:
52
+ # :iss (issuer)
53
+ # :aud (audience)
54
+ # :sub (subject) -- formerly :prn https://datatracker.ietf.org/doc/html/draft-ietf-oauth-json-web-token-06#appendix-F
55
+ # :exp, (expiration time) -- in seconds, e.g. Time.now.utc.to_i + 3600
56
+ #
57
+ # Note that this method does *not* validate presence of those four claim keys indicated as required by RFC 7523.
58
+ # There are endpoints that may not conform with this RFC, and this gem should still work for those use cases.
59
+ #
60
+ # @param [Hash] encoding_opts a hash containing instructions on how the JWT should be encoded
61
+ # @option algorithm [String] the algorithm with which you would like the JWT to be encoded
62
+ # @option key [Object] the key with which you would like to encode the JWT
63
+ #
64
+ # These two options are passed directly to `JWT.encode`. For supported encoding arguments:
65
+ # @see https://github.com/jwt/ruby-jwt#algorithms-and-usage
66
+ # @see https://datatracker.ietf.org/doc/html/rfc7518#section-3.1
37
67
  #
38
- # params :hmac_secret, secret string.
39
- # params :private_key, private key string.
68
+ # The object type of `:key` may depend on the value of `:algorithm`. Sample arguments:
69
+ # `get_token(claim_set, {:algorithm => 'HS256', :key => 'secret_key'})`
70
+ # `get_token(claim_set, {:algorithm => 'RS256', :key => OpenSSL::PKCS12.new(File.read('my_key.p12'), 'not_secret')})`
40
71
  #
41
- # params :iss, issuer
42
- # params :aud, audience, optional
43
- # params :prn, principal, current user
44
- # params :exp, expired at, in seconds, like Time.now.utc.to_i + 3600
72
+ # @param [Hash] request_opts options that will be used to assemble the request
73
+ # @option request_opts [String] :scope the url parameter `scope` that may be required by some endpoints
74
+ # @see https://datatracker.ietf.org/doc/html/rfc7521#section-4.1
45
75
  #
46
- # @param [Hash] opts options
47
- def get_token(params = {}, opts = {})
48
- hash = build_request(params)
49
- @client.get_token(hash, opts.merge('refresh_token' => nil))
76
+ # @param [Hash] response_opts this will be merged with the token response to create the AccessToken object
77
+ # @see the access_token_opts argument to Client#get_token
78
+
79
+ def get_token(claims, encoding_opts, request_opts = {}, response_opts = {})
80
+ assertion = build_assertion(claims, encoding_opts)
81
+ params = build_request(assertion, request_opts)
82
+
83
+ @client.get_token(params, response_opts)
50
84
  end
51
85
 
52
- def build_request(params)
53
- assertion = build_assertion(params)
86
+ private
87
+
88
+ def build_request(assertion, request_opts = {})
54
89
  {
55
- :grant_type => 'assertion',
56
- :assertion_type => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
57
- :assertion => assertion,
58
- :scope => params[:scope],
59
- }
90
+ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
91
+ assertion: assertion,
92
+ }.merge(request_opts)
60
93
  end
61
94
 
62
- def build_assertion(params)
63
- claims = {
64
- :iss => params[:iss],
65
- :aud => params[:aud],
66
- :prn => params[:prn],
67
- :exp => params[:exp],
68
- }
69
- if params[:hmac_secret]
70
- JWT.encode(claims, params[:hmac_secret], 'HS256')
71
- elsif params[:private_key]
72
- JWT.encode(claims, params[:private_key], 'RS256')
73
- end
95
+ def build_assertion(claims, encoding_opts)
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
+
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)
74
102
  end
75
103
  end
76
104
  end
@@ -4,19 +4,30 @@ 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
10
20
  #
11
21
  # @param [Hash] params additional query parameters
12
22
  def authorize_params(params = {})
13
- params.merge('response_type' => 'code', 'client_id' => @client.id)
23
+ params.merge("response_type" => "code", "client_id" => @client.id)
14
24
  end
15
25
 
16
26
  # The authorization URL endpoint of the provider
17
27
  #
18
28
  # @param [Hash] params additional query parameters for the URL
19
29
  def authorize_url(params = {})
30
+ assert_valid_params(params)
20
31
  @client.authorize_url(authorize_params.merge(params))
21
32
  end
22
33
 
@@ -24,12 +35,22 @@ module OAuth2
24
35
  #
25
36
  # @param [String] code The Authorization Code value
26
37
  # @param [Hash] params additional params
27
- # @param [Hash] opts options
38
+ # @param [Hash] opts access_token_opts, @see Client#get_token
28
39
  # @note that you must also provide a :redirect_uri with most OAuth 2.0 providers
29
40
  def get_token(code, params = {}, opts = {})
30
- params = {'grant_type' => 'authorization_code', 'code' => code}.merge(@client.redirection_params).merge(params)
41
+ params = {"grant_type" => "authorization_code", "code" => code}.merge(@client.redirection_params).merge(params)
42
+ params_dup = params.dup
43
+ params.each_key do |key|
44
+ params_dup[key.to_s] = params_dup.delete(key) if key.is_a?(Symbol)
45
+ end
46
+
47
+ @client.get_token(params_dup, opts)
48
+ end
49
+
50
+ private
31
51
 
32
- @client.get_token(params, opts)
52
+ def assert_valid_params(params)
53
+ raise(ArgumentError, "client_secret is not allowed in authorize URL query params") if params.key?(:client_secret) || params.key?("client_secret")
33
54
  end
34
55
  end
35
56
  end
File without changes
@@ -10,7 +10,7 @@ module OAuth2
10
10
  #
11
11
  # @raise [NotImplementedError]
12
12
  def authorize_url
13
- raise(NotImplementedError, 'The authorization endpoint is not used in this strategy')
13
+ raise(NotImplementedError, "The authorization endpoint is not used in this strategy")
14
14
  end
15
15
 
16
16
  # Retrieve an access token given the specified client.
@@ -18,8 +18,8 @@ module OAuth2
18
18
  # @param [Hash] params additional params
19
19
  # @param [Hash] opts options
20
20
  def get_token(params = {}, opts = {})
21
- params = params.merge('grant_type' => 'client_credentials')
22
- @client.get_token(params, opts.merge('refresh_token' => nil))
21
+ params = params.merge("grant_type" => "client_credentials")
22
+ @client.get_token(params, opts)
23
23
  end
24
24
  end
25
25
  end
@@ -4,19 +4,28 @@ 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
10
18
  #
11
19
  # @param [Hash] params additional query parameters
12
20
  def authorize_params(params = {})
13
- params.merge('response_type' => 'token', 'client_id' => @client.id)
21
+ params.merge("response_type" => "token", "client_id" => @client.id)
14
22
  end
15
23
 
16
24
  # The authorization URL endpoint of the provider
17
25
  #
18
26
  # @param [Hash] params additional query parameters for the URL
19
27
  def authorize_url(params = {})
28
+ assert_valid_params(params)
20
29
  @client.authorize_url(authorize_params.merge(params))
21
30
  end
22
31
 
@@ -24,7 +33,13 @@ module OAuth2
24
33
  #
25
34
  # @raise [NotImplementedError]
26
35
  def get_token(*)
27
- raise(NotImplementedError, 'The token is accessed differently in this strategy')
36
+ raise(NotImplementedError, "The token is accessed differently in this strategy")
37
+ end
38
+
39
+ private
40
+
41
+ def assert_valid_params(params)
42
+ raise(ArgumentError, "client_secret is not allowed in authorize URL query params") if params.key?(:client_secret) || params.key?("client_secret")
28
43
  end
29
44
  end
30
45
  end
@@ -4,13 +4,21 @@ 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
10
18
  #
11
19
  # @raise [NotImplementedError]
12
20
  def authorize_url
13
- raise(NotImplementedError, 'The authorization endpoint is not used in this strategy')
21
+ raise(NotImplementedError, "The authorization endpoint is not used in this strategy")
14
22
  end
15
23
 
16
24
  # Retrieve an access token given the specified End User username and password.
@@ -19,9 +27,11 @@ module OAuth2
19
27
  # @param [String] password the End User password
20
28
  # @param [Hash] params additional params
21
29
  def get_token(username, password, params = {}, opts = {})
22
- params = {'grant_type' => 'password',
23
- 'username' => username,
24
- 'password' => password}.merge(params)
30
+ params = {
31
+ "grant_type" => "password",
32
+ "username" => username,
33
+ "password" => password,
34
+ }.merge(params)
25
35
  @client.get_token(params, opts)
26
36
  end
27
37
  end