oauth2 2.0.9 → 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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +662 -183
- data/CITATION.cff +20 -0
- data/CODE_OF_CONDUCT.md +24 -23
- data/CONTRIBUTING.md +219 -34
- data/FUNDING.md +77 -0
- data/{LICENSE → LICENSE.txt} +2 -2
- data/OIDC.md +158 -0
- data/README.md +1428 -306
- data/REEK +0 -0
- data/RUBOCOP.md +71 -0
- data/SECURITY.md +11 -16
- data/lib/oauth2/access_token.rb +236 -38
- data/lib/oauth2/authenticator.rb +39 -7
- data/lib/oauth2/client.rb +311 -96
- data/lib/oauth2/error.rb +35 -17
- data/lib/oauth2/filtered_attributes.rb +52 -0
- data/lib/oauth2/response.rb +78 -42
- data/lib/oauth2/strategy/assertion.rb +8 -5
- data/lib/oauth2/strategy/auth_code.rb +13 -3
- data/lib/oauth2/strategy/client_credentials.rb +2 -2
- data/lib/oauth2/strategy/implicit.rb +11 -3
- data/lib/oauth2/strategy/password.rb +14 -4
- data/lib/oauth2/version.rb +1 -1
- data/lib/oauth2.rb +59 -18
- data/sig/oauth2/access_token.rbs +25 -0
- data/sig/oauth2/authenticator.rbs +22 -0
- data/sig/oauth2/client.rbs +52 -0
- data/sig/oauth2/error.rbs +8 -0
- data/sig/oauth2/filtered_attributes.rbs +6 -0
- data/sig/oauth2/response.rbs +18 -0
- data/sig/oauth2/strategy.rbs +34 -0
- data/sig/oauth2/version.rbs +5 -0
- data/sig/oauth2.rbs +9 -0
- data.tar.gz.sig +0 -0
- metadata +292 -75
- metadata.gz.sig +0 -0
data/lib/oauth2/error.rb
CHANGED
|
@@ -1,55 +1,73 @@
|
|
|
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
|
|
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
|
|
5
13
|
attr_reader :response, :body, :code, :description
|
|
6
14
|
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
15
|
+
# Create a new OAuth2::Error
|
|
16
|
+
#
|
|
17
|
+
# @param [OAuth2::Response, Hash, Object] response A Response or error payload
|
|
10
18
|
def initialize(response)
|
|
11
19
|
@response = response
|
|
12
20
|
if response.respond_to?(:parsed)
|
|
13
21
|
if response.parsed.is_a?(Hash)
|
|
14
|
-
@code = response.parsed[
|
|
15
|
-
@description = response.parsed[
|
|
22
|
+
@code = response.parsed["error"]
|
|
23
|
+
@description = response.parsed["error_description"]
|
|
16
24
|
end
|
|
17
25
|
elsif response.is_a?(Hash)
|
|
18
|
-
@code = response[
|
|
19
|
-
@description = response[
|
|
26
|
+
@code = response["error"]
|
|
27
|
+
@description = response["error_description"]
|
|
20
28
|
end
|
|
21
29
|
@body = if response.respond_to?(:body)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
30
|
+
response.body
|
|
31
|
+
else
|
|
32
|
+
@response
|
|
33
|
+
end
|
|
26
34
|
message_opts = parse_error_description(@code, @description)
|
|
27
35
|
super(error_message(@body, message_opts))
|
|
28
36
|
end
|
|
29
37
|
|
|
30
38
|
private
|
|
31
39
|
|
|
40
|
+
# Builds a multi-line error message including description and raw body.
|
|
41
|
+
#
|
|
42
|
+
# @param [String, #encode] response_body Response body content
|
|
43
|
+
# @param [Hash] opts Options including :error_description
|
|
44
|
+
# @return [String] Message suitable for StandardError
|
|
32
45
|
def error_message(response_body, opts = {})
|
|
33
46
|
lines = []
|
|
34
47
|
|
|
35
48
|
lines << opts[:error_description] if opts[:error_description]
|
|
36
49
|
|
|
37
50
|
error_string = if response_body.respond_to?(:encode) && opts[:error_description].respond_to?(:encoding)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
51
|
+
script_encoding = opts[:error_description].encoding
|
|
52
|
+
response_body.encode(script_encoding, invalid: :replace, undef: :replace)
|
|
53
|
+
else
|
|
54
|
+
response_body
|
|
55
|
+
end
|
|
43
56
|
|
|
44
57
|
lines << error_string
|
|
45
58
|
|
|
46
59
|
lines.join("\n")
|
|
47
60
|
end
|
|
48
61
|
|
|
62
|
+
# Formats the OAuth2 error code and description into a single string.
|
|
63
|
+
#
|
|
64
|
+
# @param [String, nil] code OAuth2 error code
|
|
65
|
+
# @param [String, nil] description OAuth2 error description
|
|
66
|
+
# @return [Hash] Options hash containing :error_description when present
|
|
49
67
|
def parse_error_description(code, description)
|
|
50
68
|
return {} unless code || description
|
|
51
69
|
|
|
52
|
-
error_description =
|
|
70
|
+
error_description = ""
|
|
53
71
|
error_description += "#{code}: " if code
|
|
54
72
|
error_description += description if description
|
|
55
73
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
module OAuth2
|
|
2
|
+
# Mixin that redacts sensitive instance variables in #inspect output.
|
|
3
|
+
#
|
|
4
|
+
# Classes include this module and declare which attributes should be filtered
|
|
5
|
+
# using {.filtered_attributes}. Any instance variable name that includes one of
|
|
6
|
+
# those attribute names will be shown as [FILTERED] in the object's inspect.
|
|
7
|
+
module FilteredAttributes
|
|
8
|
+
# Hook invoked when the module is included. Extends the including class with
|
|
9
|
+
# class-level helpers.
|
|
10
|
+
#
|
|
11
|
+
# @param [Class] base The including class
|
|
12
|
+
# @return [void]
|
|
13
|
+
def self.included(base)
|
|
14
|
+
base.extend(ClassMethods)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Class-level helpers for configuring filtered attributes.
|
|
18
|
+
module ClassMethods
|
|
19
|
+
# Declare attributes that should be redacted in inspect output.
|
|
20
|
+
#
|
|
21
|
+
# @param [Array<Symbol, String>] attributes One or more attribute names
|
|
22
|
+
# @return [void]
|
|
23
|
+
def filtered_attributes(*attributes)
|
|
24
|
+
@filtered_attribute_names = attributes.map(&:to_sym)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# The configured attribute names to filter.
|
|
28
|
+
#
|
|
29
|
+
# @return [Array<Symbol>]
|
|
30
|
+
def filtered_attribute_names
|
|
31
|
+
@filtered_attribute_names || []
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Custom inspect that redacts configured attributes.
|
|
36
|
+
#
|
|
37
|
+
# @return [String]
|
|
38
|
+
def inspect
|
|
39
|
+
filtered_attribute_names = self.class.filtered_attribute_names
|
|
40
|
+
return super if filtered_attribute_names.empty?
|
|
41
|
+
|
|
42
|
+
inspected_vars = instance_variables.map do |var|
|
|
43
|
+
if filtered_attribute_names.any? { |filtered_var| var.to_s.include?(filtered_var.to_s) }
|
|
44
|
+
"#{var}=[FILTERED]"
|
|
45
|
+
else
|
|
46
|
+
"#{var}=#{instance_variable_get(var).inspect}"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
"#<#{self.class}:#{object_id} #{inspected_vars.join(", ")}>"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
data/lib/oauth2/response.rb
CHANGED
|
@@ -1,37 +1,55 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
5
|
-
require
|
|
3
|
+
require "json"
|
|
4
|
+
require "multi_xml"
|
|
5
|
+
require "rack"
|
|
6
6
|
|
|
7
7
|
module OAuth2
|
|
8
|
-
#
|
|
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
|
-
#
|
|
18
|
-
#
|
|
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
|
-
#
|
|
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
|
-
|
|
27
|
-
|
|
42
|
+
"application/x-www-form-urlencoded" => :query,
|
|
43
|
+
"text/plain" => :text,
|
|
28
44
|
}
|
|
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]
|
|
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)
|
|
47
|
-
#
|
|
48
|
-
# @param [
|
|
49
|
-
#
|
|
50
|
-
# @
|
|
51
|
-
|
|
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
|
-
response.body ||
|
|
99
|
+
response.body || ""
|
|
72
100
|
end
|
|
73
101
|
|
|
74
|
-
# The
|
|
102
|
+
# The parsed response body
|
|
75
103
|
#
|
|
76
|
-
# @return [Object]
|
|
77
|
-
# @return [nil] If
|
|
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
|
|
|
@@ -90,34 +118,37 @@ module OAuth2
|
|
|
90
118
|
end
|
|
91
119
|
end
|
|
92
120
|
|
|
93
|
-
|
|
121
|
+
if options[:snaky] && @parsed.is_a?(Hash)
|
|
122
|
+
hash_klass = options[:snaky_hash_klass] || DEFAULT_OPTIONS[:snaky_hash_klass]
|
|
123
|
+
@parsed = hash_klass[@parsed]
|
|
124
|
+
end
|
|
94
125
|
|
|
95
126
|
@parsed
|
|
96
127
|
end
|
|
97
128
|
|
|
98
|
-
#
|
|
129
|
+
# Determines the content type of the response
|
|
130
|
+
#
|
|
131
|
+
# @return [String, nil] The content type or nil if headers are not present
|
|
99
132
|
def content_type
|
|
100
|
-
return
|
|
133
|
+
return unless response.headers
|
|
101
134
|
|
|
102
|
-
((response.headers.values_at(
|
|
135
|
+
((response.headers.values_at("content-type", "Content-Type").compact.first || "").split(";").first || "").strip.downcase
|
|
103
136
|
end
|
|
104
137
|
|
|
105
|
-
# Determines the parser
|
|
106
|
-
# that will be passed the {#body} (and optional {#response}) to supply
|
|
107
|
-
# {#parsed}.
|
|
138
|
+
# Determines the parser to be used for the response body
|
|
108
139
|
#
|
|
109
|
-
# The parser can be supplied as the +:parse+ option in the form of a Proc
|
|
110
|
-
#
|
|
111
|
-
#
|
|
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.
|
|
112
143
|
#
|
|
113
|
-
# If no +:parse+ option is supplied, the lookup Symbol will be determined
|
|
114
|
-
#
|
|
144
|
+
# @note If no +:parse+ option is supplied, the lookup Symbol will be determined
|
|
145
|
+
# by looking up {#content_type} in {@@content_types}.
|
|
115
146
|
#
|
|
116
|
-
# If {#parser} is a Proc, it will be called with no arguments, just
|
|
117
|
-
#
|
|
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.
|
|
118
149
|
#
|
|
119
|
-
# @return [Proc, #call]
|
|
120
|
-
# @return [nil] If no parser
|
|
150
|
+
# @return [Proc, #call] The parser proc or callable object
|
|
151
|
+
# @return [nil] If no suitable parser is found
|
|
121
152
|
def parser
|
|
122
153
|
return @parser if defined?(@parser)
|
|
123
154
|
|
|
@@ -133,16 +164,21 @@ module OAuth2
|
|
|
133
164
|
end
|
|
134
165
|
end
|
|
135
166
|
|
|
136
|
-
|
|
167
|
+
# Register XML parser
|
|
168
|
+
# @api private
|
|
169
|
+
OAuth2::Response.register_parser(:xml, ["text/xml", "application/rss+xml", "application/rdf+xml", "application/atom+xml", "application/xml"]) do |body|
|
|
137
170
|
next body unless body.respond_to?(:to_str)
|
|
138
171
|
|
|
139
172
|
MultiXml.parse(body)
|
|
140
173
|
end
|
|
141
174
|
|
|
142
|
-
|
|
175
|
+
# Register JSON parser
|
|
176
|
+
# @api private
|
|
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|
|
|
143
178
|
next body unless body.respond_to?(:to_str)
|
|
144
179
|
|
|
145
|
-
body = body.dup.force_encoding(
|
|
180
|
+
body = body.dup.force_encoding(Encoding::ASCII_8BIT) if body.respond_to?(:force_encoding)
|
|
181
|
+
next body if body.respond_to?(:empty?) && body.empty?
|
|
146
182
|
|
|
147
|
-
|
|
183
|
+
JSON.parse(body)
|
|
148
184
|
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
3
|
+
require "jwt"
|
|
4
4
|
|
|
5
5
|
module OAuth2
|
|
6
6
|
module Strategy
|
|
@@ -34,7 +34,7 @@ module OAuth2
|
|
|
34
34
|
#
|
|
35
35
|
# @raise [NotImplementedError]
|
|
36
36
|
def authorize_url
|
|
37
|
-
raise(NotImplementedError,
|
|
37
|
+
raise(NotImplementedError, "The authorization endpoint is not used in this strategy")
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
# Retrieve an access token given the specified client.
|
|
@@ -87,15 +87,18 @@ module OAuth2
|
|
|
87
87
|
|
|
88
88
|
def build_request(assertion, request_opts = {})
|
|
89
89
|
{
|
|
90
|
-
grant_type:
|
|
90
|
+
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
|
91
91
|
assertion: assertion,
|
|
92
92
|
}.merge(request_opts)
|
|
93
93
|
end
|
|
94
94
|
|
|
95
95
|
def build_assertion(claims, encoding_opts)
|
|
96
|
-
raise ArgumentError.new(message:
|
|
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
|
-
|
|
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,13 +4,23 @@ 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(
|
|
23
|
+
params.merge("response_type" => "code", "client_id" => @client.id)
|
|
14
24
|
end
|
|
15
25
|
|
|
16
26
|
# The authorization URL endpoint of the provider
|
|
@@ -28,7 +38,7 @@ module OAuth2
|
|
|
28
38
|
# @param [Hash] opts access_token_opts, @see Client#get_token
|
|
29
39
|
# @note that you must also provide a :redirect_uri with most OAuth 2.0 providers
|
|
30
40
|
def get_token(code, params = {}, opts = {})
|
|
31
|
-
params = {
|
|
41
|
+
params = {"grant_type" => "authorization_code", "code" => code}.merge(@client.redirection_params).merge(params)
|
|
32
42
|
params_dup = params.dup
|
|
33
43
|
params.each_key do |key|
|
|
34
44
|
params_dup[key.to_s] = params_dup.delete(key) if key.is_a?(Symbol)
|
|
@@ -40,7 +50,7 @@ module OAuth2
|
|
|
40
50
|
private
|
|
41
51
|
|
|
42
52
|
def assert_valid_params(params)
|
|
43
|
-
raise(ArgumentError,
|
|
53
|
+
raise(ArgumentError, "client_secret is not allowed in authorize URL query params") if params.key?(:client_secret) || params.key?("client_secret")
|
|
44
54
|
end
|
|
45
55
|
end
|
|
46
56
|
end
|
|
@@ -10,7 +10,7 @@ module OAuth2
|
|
|
10
10
|
#
|
|
11
11
|
# @raise [NotImplementedError]
|
|
12
12
|
def authorize_url
|
|
13
|
-
raise(NotImplementedError,
|
|
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,7 +18,7 @@ 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(
|
|
21
|
+
params = params.merge("grant_type" => "client_credentials")
|
|
22
22
|
@client.get_token(params, opts)
|
|
23
23
|
end
|
|
24
24
|
end
|
|
@@ -4,13 +4,21 @@ 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(
|
|
21
|
+
params.merge("response_type" => "token", "client_id" => @client.id)
|
|
14
22
|
end
|
|
15
23
|
|
|
16
24
|
# The authorization URL endpoint of the provider
|
|
@@ -25,13 +33,13 @@ module OAuth2
|
|
|
25
33
|
#
|
|
26
34
|
# @raise [NotImplementedError]
|
|
27
35
|
def get_token(*)
|
|
28
|
-
raise(NotImplementedError,
|
|
36
|
+
raise(NotImplementedError, "The token is accessed differently in this strategy")
|
|
29
37
|
end
|
|
30
38
|
|
|
31
39
|
private
|
|
32
40
|
|
|
33
41
|
def assert_valid_params(params)
|
|
34
|
-
raise(ArgumentError,
|
|
42
|
+
raise(ArgumentError, "client_secret is not allowed in authorize URL query params") if params.key?(:client_secret) || params.key?("client_secret")
|
|
35
43
|
end
|
|
36
44
|
end
|
|
37
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,
|
|
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 = {
|
|
23
|
-
|
|
24
|
-
|
|
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
|
data/lib/oauth2/version.rb
CHANGED
data/lib/oauth2.rb
CHANGED
|
@@ -1,40 +1,81 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# includes modules from stdlib
|
|
4
|
-
require
|
|
5
|
-
require
|
|
4
|
+
require "cgi"
|
|
5
|
+
require "time"
|
|
6
6
|
|
|
7
7
|
# third party gems
|
|
8
|
-
require
|
|
9
|
-
require
|
|
8
|
+
require "snaky_hash"
|
|
9
|
+
require "version_gem"
|
|
10
10
|
|
|
11
11
|
# includes gem files
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
12
|
+
require_relative "oauth2/version"
|
|
13
|
+
require_relative "oauth2/filtered_attributes"
|
|
14
|
+
require_relative "oauth2/error"
|
|
15
|
+
require_relative "oauth2/authenticator"
|
|
16
|
+
require_relative "oauth2/client"
|
|
17
|
+
require_relative "oauth2/strategy/base"
|
|
18
|
+
require_relative "oauth2/strategy/auth_code"
|
|
19
|
+
require_relative "oauth2/strategy/implicit"
|
|
20
|
+
require_relative "oauth2/strategy/password"
|
|
21
|
+
require_relative "oauth2/strategy/client_credentials"
|
|
22
|
+
require_relative "oauth2/strategy/assertion"
|
|
23
|
+
require_relative "oauth2/access_token"
|
|
24
|
+
require_relative "oauth2/response"
|
|
24
25
|
|
|
25
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.
|
|
26
30
|
module OAuth2
|
|
27
|
-
|
|
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]
|
|
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
|
|
47
|
+
DEFAULT_CONFIG = SnakyHash::SymbolKeyed.new(
|
|
48
|
+
silence_extra_tokens_warning: true,
|
|
49
|
+
silence_no_tokens_warning: true,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# The current runtime configuration for the library.
|
|
53
|
+
#
|
|
54
|
+
# @return [SnakyHash::SymbolKeyed]
|
|
28
55
|
@config = DEFAULT_CONFIG.dup
|
|
56
|
+
|
|
29
57
|
class << self
|
|
30
|
-
|
|
58
|
+
# Access the current configuration.
|
|
59
|
+
#
|
|
60
|
+
# Prefer using {OAuth2.configure} to mutate configuration.
|
|
61
|
+
#
|
|
62
|
+
# @return [SnakyHash::SymbolKeyed]
|
|
63
|
+
attr_reader :config
|
|
31
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]
|
|
32
72
|
def configure
|
|
33
73
|
yield @config
|
|
34
74
|
end
|
|
35
75
|
module_function :configure
|
|
36
76
|
end
|
|
37
77
|
|
|
78
|
+
# Extend OAuth2::Version with VersionGem helpers to provide semantic version helpers.
|
|
38
79
|
OAuth2::Version.class_eval do
|
|
39
80
|
extend VersionGem::Basic
|
|
40
81
|
end
|