oauth2 1.4.11 → 2.0.25

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.
data/lib/oauth2/error.rb CHANGED
@@ -1,42 +1,81 @@
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
+ parsed_response = response.parsed
24
+ if parsed_response.is_a?(Hash)
25
+ @code = parsed_response["error"]
26
+ @description = parsed_response["error_description"]
27
+ end
28
+ elsif response.is_a?(Hash)
29
+ @code = response["error"]
30
+ @description = response["error_description"]
17
31
  end
18
-
19
- super(error_message(response.body, :error_description => error_description))
32
+ @body = if response.respond_to?(:body)
33
+ response.body
34
+ else
35
+ @response
36
+ end
37
+ message_opts = parse_error_description(@code, @description)
38
+ super(error_message(@body, message_opts))
20
39
  end
21
40
 
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
41
+ private
42
+
43
+ # Builds a multi-line error message including description and raw body.
44
+ #
45
+ # @param [String, #encode] response_body Response body content
46
+ # @param [Hash] opts Options including :error_description
47
+ # @return [String] Message suitable for StandardError
25
48
  def error_message(response_body, opts = {})
26
- message = []
49
+ lines = []
50
+ error_description = opts[:error_description]
51
+
52
+ lines << error_description if error_description
27
53
 
28
- opts[:error_description] && (message << opts[:error_description])
54
+ error_string = if response_body.respond_to?(:encode) && error_description.respond_to?(:encoding)
55
+ script_encoding = error_description.encoding
56
+ response_body.encode(script_encoding, invalid: :replace, undef: :replace)
57
+ else
58
+ response_body
59
+ end
60
+
61
+ lines << error_string
62
+
63
+ lines.join("\n")
64
+ end
29
65
 
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
66
+ # Formats the OAuth2 error code and description into a single string.
67
+ #
68
+ # @param [String, nil] code OAuth2 error code
69
+ # @param [String, nil] description OAuth2 error description
70
+ # @return [Hash] Options hash containing :error_description when present
71
+ def parse_error_description(code, description)
72
+ return {} unless code || description
36
73
 
37
- message << error_message
74
+ error_description = ""
75
+ error_description += "#{code}: " if code
76
+ error_description += description if description
38
77
 
39
- message.join("\n")
78
+ {error_description: error_description}
40
79
  end
41
80
  end
42
81
  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,190 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'multi_json'
4
- require 'multi_xml'
5
- require 'rack'
3
+ require "json"
4
+ require "set"
5
+ require "multi_xml"
6
+ require "rack"
6
7
 
7
8
  module OAuth2
8
- # OAuth2::Response class
9
+ # The Response class handles HTTP responses in the OAuth2 gem, providing methods
10
+ # to access and parse response data in various formats.
11
+ #
12
+ # @since 1.0.0
9
13
  class Response
14
+ # Default configuration options for Response instances
15
+ #
16
+ # @return [Hash] The default options hash
17
+ DEFAULT_OPTIONS = {
18
+ parse: :automatic,
19
+ snaky: true,
20
+ snaky_hash_klass: SnakyHash::StringKeyed,
21
+ }.freeze
22
+
23
+ # @return [Faraday::Response] The raw Faraday response object
10
24
  attr_reader :response
11
- attr_accessor :error, :options
12
25
 
13
- # Procs that, when called, will parse a response body according
14
- # to the specified format.
26
+ # @return [Hash] The options hash for this instance
27
+ attr_accessor :options
28
+
29
+ # @private
30
+ # Storage for response body parser procedures
31
+ #
32
+ # @return [Hash<Symbol, Proc>] Hash of parser procs keyed by format symbol
15
33
  @@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 },
34
+ query: ->(body) { Rack::Utils.parse_query(body) },
35
+ text: ->(body) { body },
19
36
  }
20
37
 
21
- # Content type assignments for various potential HTTP content types.
38
+ # @private
39
+ # Maps content types to parser symbols
40
+ #
41
+ # @return [Hash<String, Symbol>] Hash of content types mapped to parser symbols
22
42
  @@content_types = {
23
- 'application/json' => :json,
24
- 'text/javascript' => :json,
25
- 'application/x-www-form-urlencoded' => :query,
26
- 'text/plain' => :text,
43
+ "application/x-www-form-urlencoded" => :query,
44
+ "text/plain" => :text,
27
45
  }
28
46
 
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
47
+ class << self
48
+ # Adds a new content type parser.
49
+ #
50
+ # @param [Symbol] key A descriptive symbol key such as :json or :query
51
+ # @param [Array<String>, String] mime_types One or more mime types to which this parser applies
52
+ # @yield [String] Block that will be called to parse the response body
53
+ # @yieldparam [String] body The response body to parse
54
+ # @return [void]
55
+ def register_parser(key, mime_types, &block)
56
+ key = key.to_sym
57
+ @@parsers[key] = block
58
+ Array(mime_types).each do |mime_type|
59
+ @@content_types[mime_type] = key
60
+ end
39
61
  end
40
62
  end
41
63
 
42
64
  # Initializes a Response instance
43
65
  #
44
66
  # @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 = {})
67
+ # @param [Symbol] parse (:automatic) How to parse the response body
68
+ # @param [Boolean] snaky (true) Whether to convert parsed response to snake_case using SnakyHash
69
+ # @param [Class, nil] snaky_hash_klass (nil) Custom class for snake_case hash conversion
70
+ # @param [Hash] options Additional options for the response
71
+ # @option options [Symbol] :parse (:automatic) Parse strategy (:query, :json, or :automatic)
72
+ # @option options [Boolean] :snaky (true) Enable/disable snake_case conversion
73
+ # @option options [Class] :snaky_hash_klass (SnakyHash::StringKeyed) Class to use for hash conversion
74
+ # @return [OAuth2::Response] The new Response instance
75
+ def initialize(response, parse: :automatic, snaky: true, snaky_hash_klass: nil, **options)
49
76
  @response = response
50
- @options = {:parse => :automatic}.merge(opts)
77
+ @options = {
78
+ parse: parse,
79
+ snaky: snaky,
80
+ snaky_hash_klass: snaky_hash_klass,
81
+ }.merge(options)
51
82
  end
52
83
 
53
84
  # The HTTP response headers
85
+ #
86
+ # @return [Hash] The response headers
54
87
  def headers
55
88
  response.headers
56
89
  end
57
90
 
58
91
  # The HTTP response status code
92
+ #
93
+ # @return [Integer] The response status code
59
94
  def status
60
95
  response.status
61
96
  end
62
97
 
63
98
  # The HTTP response body
99
+ #
100
+ # @return [String] The response body or empty string if nil
64
101
  def body
65
- response.body || ''
102
+ response.body || ""
66
103
  end
67
104
 
68
- # The parsed response body.
69
- # Will attempt to parse application/x-www-form-urlencoded and
70
- # application/json Content-Type response bodies
105
+ # The parsed response body
106
+ #
107
+ # @return [Object, SnakyHash::StringKeyed] The parsed response body
108
+ # @return [nil] If no parser is available
71
109
  def parsed
72
- return nil unless @@parsers.key?(parser)
110
+ return @parsed if defined?(@parsed)
111
+
112
+ response_parser = parser
113
+ @parsed =
114
+ if response_parser.respond_to?(:call)
115
+ case response_parser.arity
116
+ when 0
117
+ response_parser.call
118
+ when 1
119
+ response_parser.call(body)
120
+ else
121
+ response_parser.call(body, response)
122
+ end
123
+ end
124
+
125
+ if options[:snaky] && @parsed.is_a?(Hash)
126
+ hash_klass = options[:snaky_hash_klass] || DEFAULT_OPTIONS[:snaky_hash_klass]
127
+ @parsed = hash_klass[@parsed]
128
+ end
73
129
 
74
- @parsed ||= @@parsers[parser].call(body)
130
+ @parsed
75
131
  end
76
132
 
77
- # Attempts to determine the content type of the response.
133
+ # Determines the content type of the response
134
+ #
135
+ # @return [String, nil] The content type or nil if headers are not present
78
136
  def content_type
79
- ((response.headers.values_at('content-type', 'Content-Type').compact.first || '').split(';').first || '').strip
137
+ response_headers = response.headers
138
+ return unless response_headers
139
+
140
+ ((response_headers.values_at("content-type", "Content-Type").compact.first || "").split(";").first || "").strip.downcase
80
141
  end
81
142
 
82
- # Determines the parser that will be used to supply the content of #parsed
143
+ # Determines the parser to be used for the response body
144
+ #
145
+ # @note The parser can be supplied as the +:parse+ option in the form of a Proc
146
+ # (or other Object responding to #call) or a Symbol. In the latter case,
147
+ # the actual parser will be looked up in {@@parsers} by the supplied Symbol.
148
+ #
149
+ # @note If no +:parse+ option is supplied, the lookup Symbol will be determined
150
+ # by looking up {#content_type} in {@@content_types}.
151
+ #
152
+ # @note If {#parser} is a Proc, it will be called with no arguments, just
153
+ # {#body}, or {#body} and {#response}, depending on the Proc's arity.
154
+ #
155
+ # @return [Proc, #call] The parser proc or callable object
156
+ # @return [nil] If no suitable parser is found
83
157
  def parser
84
- return options[:parse].to_sym if @@parsers.key?(options[:parse])
158
+ return @parser if defined?(@parser)
85
159
 
86
- @@content_types[content_type]
160
+ parse_option = options[:parse]
161
+ @parser =
162
+ if parse_option.respond_to?(:call)
163
+ parse_option
164
+ elsif parse_option
165
+ @@parsers[parse_option.to_sym]
166
+ end
167
+
168
+ @parser ||= @@parsers[@@content_types[content_type]]
87
169
  end
88
170
  end
89
171
  end
90
172
 
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
173
+ # Register XML parser
174
+ # @api private
175
+ OAuth2::Response.register_parser(:xml, ["text/xml", "application/rss+xml", "application/rdf+xml", "application/atom+xml", "application/xml"]) do |body|
176
+ next body unless body.respond_to?(:to_str)
177
+
178
+ (defined?(MultiXML) ? MultiXML : MultiXml).parse(body)
179
+ end
180
+
181
+ # Register JSON parser
182
+ # @api private
183
+ 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|
184
+ next body unless body.respond_to?(:to_str)
185
+
186
+ body = body.dup.force_encoding(Encoding::ASCII_8BIT) if body.respond_to?(:force_encoding)
187
+ next body if body.respond_to?(:empty?) && body.empty?
188
+
189
+ JSON.parse(body)
93
190
  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
@@ -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