oauth2 2.0.9 → 2.0.22
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 +3 -0
- data/CHANGELOG.md +833 -182
- data/CITATION.cff +20 -0
- data/CODE_OF_CONDUCT.md +24 -23
- data/CONTRIBUTING.md +262 -34
- data/FUNDING.md +74 -0
- data/LICENSE.md +110 -0
- data/README.md +923 -351
- data/RUBOCOP.md +71 -0
- data/SECURITY.md +11 -16
- data/certs/pboling.pem +27 -0
- data/lib/oauth2/access_token.rb +233 -38
- data/lib/oauth2/auth_sanitizer.rb +36 -0
- data/lib/oauth2/authenticator.rb +43 -9
- data/lib/oauth2/client.rb +353 -97
- data/lib/oauth2/error.rb +37 -17
- data/lib/oauth2/filtered_attributes.rb +10 -0
- data/lib/oauth2/response.rb +87 -49
- data/lib/oauth2/strategy/assertion.rb +10 -7
- 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 +2 -1
- data/lib/oauth2.rb +86 -23
- 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 +11 -0
- data/sig/oauth2/response.rbs +18 -0
- data/sig/oauth2/sanitized_logger.rbs +32 -0
- data/sig/oauth2/strategy.rbs +34 -0
- data/sig/oauth2/thing_filter.rbs +10 -0
- data/sig/oauth2/version.rbs +6 -0
- data/sig/oauth2.rbs +9 -0
- data.tar.gz.sig +0 -0
- metadata +270 -76
- metadata.gz.sig +0 -0
- data/LICENSE +0 -22
data/lib/oauth2/error.rb
CHANGED
|
@@ -1,55 +1,75 @@
|
|
|
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
|
|
20
|
+
@code = nil
|
|
21
|
+
@description = nil
|
|
12
22
|
if response.respond_to?(:parsed)
|
|
13
23
|
if response.parsed.is_a?(Hash)
|
|
14
|
-
@code = response.parsed[
|
|
15
|
-
@description = response.parsed[
|
|
24
|
+
@code = response.parsed["error"]
|
|
25
|
+
@description = response.parsed["error_description"]
|
|
16
26
|
end
|
|
17
27
|
elsif response.is_a?(Hash)
|
|
18
|
-
@code = response[
|
|
19
|
-
@description = response[
|
|
28
|
+
@code = response["error"]
|
|
29
|
+
@description = response["error_description"]
|
|
20
30
|
end
|
|
21
31
|
@body = if response.respond_to?(:body)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
32
|
+
response.body
|
|
33
|
+
else
|
|
34
|
+
@response
|
|
35
|
+
end
|
|
26
36
|
message_opts = parse_error_description(@code, @description)
|
|
27
37
|
super(error_message(@body, message_opts))
|
|
28
38
|
end
|
|
29
39
|
|
|
30
40
|
private
|
|
31
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
|
|
32
47
|
def error_message(response_body, opts = {})
|
|
33
48
|
lines = []
|
|
34
49
|
|
|
35
50
|
lines << opts[:error_description] if opts[:error_description]
|
|
36
51
|
|
|
37
52
|
error_string = if response_body.respond_to?(:encode) && opts[:error_description].respond_to?(:encoding)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
53
|
+
script_encoding = opts[:error_description].encoding
|
|
54
|
+
response_body.encode(script_encoding, invalid: :replace, undef: :replace)
|
|
55
|
+
else
|
|
56
|
+
response_body
|
|
57
|
+
end
|
|
43
58
|
|
|
44
59
|
lines << error_string
|
|
45
60
|
|
|
46
61
|
lines.join("\n")
|
|
47
62
|
end
|
|
48
63
|
|
|
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
|
|
49
69
|
def parse_error_description(code, description)
|
|
50
70
|
return {} unless code || description
|
|
51
71
|
|
|
52
|
-
error_description =
|
|
72
|
+
error_description = ""
|
|
53
73
|
error_description += "#{code}: " if code
|
|
54
74
|
error_description += description if description
|
|
55
75
|
|
|
@@ -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
|
data/lib/oauth2/response.rb
CHANGED
|
@@ -1,80 +1,110 @@
|
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
40
60
|
end
|
|
41
61
|
end
|
|
42
62
|
|
|
43
63
|
# Initializes a Response instance
|
|
44
64
|
#
|
|
45
65
|
# @param [Faraday::Response] response The Faraday response instance
|
|
46
|
-
# @param [Symbol] parse (:automatic)
|
|
47
|
-
#
|
|
48
|
-
# @param [
|
|
49
|
-
#
|
|
50
|
-
# @
|
|
51
|
-
|
|
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)
|
|
52
75
|
@response = response
|
|
53
76
|
@options = {
|
|
54
77
|
parse: parse,
|
|
55
78
|
snaky: snaky,
|
|
79
|
+
snaky_hash_klass: snaky_hash_klass,
|
|
56
80
|
}.merge(options)
|
|
57
81
|
end
|
|
58
82
|
|
|
59
83
|
# The HTTP response headers
|
|
84
|
+
#
|
|
85
|
+
# @return [Hash] The response headers
|
|
60
86
|
def headers
|
|
61
87
|
response.headers
|
|
62
88
|
end
|
|
63
89
|
|
|
64
90
|
# The HTTP response status code
|
|
91
|
+
#
|
|
92
|
+
# @return [Integer] The response status code
|
|
65
93
|
def status
|
|
66
94
|
response.status
|
|
67
95
|
end
|
|
68
96
|
|
|
69
97
|
# The HTTP response body
|
|
98
|
+
#
|
|
99
|
+
# @return [String] The response body or empty string if nil
|
|
70
100
|
def body
|
|
71
|
-
response.body ||
|
|
101
|
+
response.body || ""
|
|
72
102
|
end
|
|
73
103
|
|
|
74
|
-
# The
|
|
104
|
+
# The parsed response body
|
|
75
105
|
#
|
|
76
|
-
# @return [Object]
|
|
77
|
-
# @return [nil] If
|
|
106
|
+
# @return [Object, SnakyHash::StringKeyed] The parsed response body
|
|
107
|
+
# @return [nil] If no parser is available
|
|
78
108
|
def parsed
|
|
79
109
|
return @parsed if defined?(@parsed)
|
|
80
110
|
|
|
@@ -90,34 +120,37 @@ module OAuth2
|
|
|
90
120
|
end
|
|
91
121
|
end
|
|
92
122
|
|
|
93
|
-
|
|
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
|
|
94
127
|
|
|
95
128
|
@parsed
|
|
96
129
|
end
|
|
97
130
|
|
|
98
|
-
#
|
|
131
|
+
# Determines the content type of the response
|
|
132
|
+
#
|
|
133
|
+
# @return [String, nil] The content type or nil if headers are not present
|
|
99
134
|
def content_type
|
|
100
|
-
return
|
|
135
|
+
return unless response.headers
|
|
101
136
|
|
|
102
|
-
((response.headers.values_at(
|
|
137
|
+
((response.headers.values_at("content-type", "Content-Type").compact.first || "").split(";").first || "").strip.downcase
|
|
103
138
|
end
|
|
104
139
|
|
|
105
|
-
# Determines the parser
|
|
106
|
-
# that will be passed the {#body} (and optional {#response}) to supply
|
|
107
|
-
# {#parsed}.
|
|
140
|
+
# Determines the parser to be used for the response body
|
|
108
141
|
#
|
|
109
|
-
# The parser can be supplied as the +:parse+ option in the form of a Proc
|
|
110
|
-
#
|
|
111
|
-
#
|
|
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.
|
|
112
145
|
#
|
|
113
|
-
# If no +:parse+ option is supplied, the lookup Symbol will be determined
|
|
114
|
-
#
|
|
146
|
+
# @note If no +:parse+ option is supplied, the lookup Symbol will be determined
|
|
147
|
+
# by looking up {#content_type} in {@@content_types}.
|
|
115
148
|
#
|
|
116
|
-
# If {#parser} is a Proc, it will be called with no arguments, just
|
|
117
|
-
#
|
|
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.
|
|
118
151
|
#
|
|
119
|
-
# @return [Proc, #call]
|
|
120
|
-
# @return [nil] If no parser
|
|
152
|
+
# @return [Proc, #call] The parser proc or callable object
|
|
153
|
+
# @return [nil] If no suitable parser is found
|
|
121
154
|
def parser
|
|
122
155
|
return @parser if defined?(@parser)
|
|
123
156
|
|
|
@@ -133,16 +166,21 @@ module OAuth2
|
|
|
133
166
|
end
|
|
134
167
|
end
|
|
135
168
|
|
|
136
|
-
|
|
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|
|
|
137
172
|
next body unless body.respond_to?(:to_str)
|
|
138
173
|
|
|
139
174
|
MultiXml.parse(body)
|
|
140
175
|
end
|
|
141
176
|
|
|
142
|
-
|
|
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|
|
|
143
180
|
next body unless body.respond_to?(:to_str)
|
|
144
181
|
|
|
145
|
-
body = body.dup.force_encoding(
|
|
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?
|
|
146
184
|
|
|
147
|
-
|
|
185
|
+
JSON.parse(body)
|
|
148
186
|
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.
|
|
@@ -66,8 +66,8 @@ module OAuth2
|
|
|
66
66
|
# @see https://datatracker.ietf.org/doc/html/rfc7518#section-3.1
|
|
67
67
|
#
|
|
68
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')})
|
|
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')})`
|
|
71
71
|
#
|
|
72
72
|
# @param [Hash] request_opts options that will be used to assemble the request
|
|
73
73
|
# @option request_opts [String] :scope the url parameter `scope` that may be required by some endpoints
|
|
@@ -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,103 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# includes modules from stdlib
|
|
4
|
-
require
|
|
5
|
-
require
|
|
4
|
+
require "cgi/escape"
|
|
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/auth_sanitizer"
|
|
14
|
+
require_relative "oauth2/filtered_attributes"
|
|
15
|
+
require_relative "oauth2/error"
|
|
16
|
+
require_relative "oauth2/authenticator"
|
|
17
|
+
require_relative "oauth2/client"
|
|
18
|
+
require_relative "oauth2/strategy/base"
|
|
19
|
+
require_relative "oauth2/strategy/auth_code"
|
|
20
|
+
require_relative "oauth2/strategy/implicit"
|
|
21
|
+
require_relative "oauth2/strategy/password"
|
|
22
|
+
require_relative "oauth2/strategy/client_credentials"
|
|
23
|
+
require_relative "oauth2/strategy/assertion"
|
|
24
|
+
require_relative "oauth2/access_token"
|
|
25
|
+
require_relative "oauth2/response"
|
|
24
26
|
|
|
25
27
|
# The namespace of this library
|
|
28
|
+
#
|
|
29
|
+
# This module is the entry point and top-level namespace for the oauth2 gem.
|
|
30
|
+
# It exposes configuration, constants, and requires the primary public classes.
|
|
26
31
|
module OAuth2
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
# When true, enables verbose HTTP logging via Faraday's logger middleware.
|
|
33
|
+
# Controlled by the OAUTH_DEBUG environment variable. Any case-insensitive
|
|
34
|
+
# value equal to "true" will enable debugging.
|
|
35
|
+
#
|
|
36
|
+
# @return [Boolean]
|
|
37
|
+
OAUTH_DEBUG = ENV.fetch("OAUTH_DEBUG", "false").casecmp("true").zero?
|
|
38
|
+
|
|
39
|
+
# Default configuration values for the oauth2 library.
|
|
40
|
+
#
|
|
41
|
+
# @example Toggle warnings
|
|
42
|
+
# OAuth2.configure do |config|
|
|
43
|
+
# config[:silence_extra_tokens_warning] = false
|
|
44
|
+
# config[:silence_no_tokens_warning] = false
|
|
45
|
+
# end
|
|
46
|
+
#
|
|
47
|
+
# @example Customize filtered output markers and debug-log value filtering by key name
|
|
48
|
+
# OAuth2.configure do |config|
|
|
49
|
+
# config[:filtered_label] = "[REDACTED]"
|
|
50
|
+
# config[:filtered_debug_keys] += ["client_assertion"]
|
|
51
|
+
# end
|
|
52
|
+
#
|
|
53
|
+
# Existing objects and logger wrappers snapshot filtering configuration during
|
|
54
|
+
# initialization. Changing these config values later affects only newly
|
|
55
|
+
# initialized objects and debug loggers.
|
|
56
|
+
#
|
|
57
|
+
# @return [SnakyHash::SymbolKeyed] A mutable Hash-like config with symbol keys
|
|
58
|
+
DEFAULT_CONFIG = SnakyHash::SymbolKeyed.new(
|
|
59
|
+
silence_extra_tokens_warning: true,
|
|
60
|
+
silence_no_tokens_warning: true,
|
|
61
|
+
filtered_label: "[FILTERED]",
|
|
62
|
+
filtered_debug_keys: %w[
|
|
63
|
+
access_token
|
|
64
|
+
refresh_token
|
|
65
|
+
id_token
|
|
66
|
+
client_secret
|
|
67
|
+
assertion
|
|
68
|
+
code_verifier
|
|
69
|
+
token
|
|
70
|
+
]
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# The current runtime configuration for the library.
|
|
74
|
+
#
|
|
75
|
+
# @return [SnakyHash::SymbolKeyed]
|
|
76
|
+
CONFIG = DEFAULT_CONFIG.dup
|
|
77
|
+
|
|
29
78
|
class << self
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
79
|
+
def config
|
|
80
|
+
CONFIG
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Configure global library behavior.
|
|
84
|
+
#
|
|
85
|
+
# Yields the mutable configuration object so callers can update settings.
|
|
86
|
+
#
|
|
87
|
+
# @yieldparam [SnakyHash::SymbolKeyed] config the configuration object
|
|
88
|
+
# @return [void]
|
|
89
|
+
def configure
|
|
90
|
+
yield config
|
|
91
|
+
end
|
|
34
92
|
end
|
|
35
|
-
module_function :configure
|
|
36
93
|
end
|
|
37
94
|
|
|
95
|
+
# Wire OAuth2::AUTH_SANITIZER's label provider to read from OAuth2.config so that
|
|
96
|
+
# FilteredAttributes-bearing objects and OAuth2::AUTH_SANITIZER::SanitizedLogger instances
|
|
97
|
+
# pick up OAuth2.config[:filtered_label] at their initialization time.
|
|
98
|
+
OAuth2::AUTH_SANITIZER.filtered_label_provider = -> { OAuth2.config[:filtered_label] }
|
|
99
|
+
|
|
100
|
+
# Extend OAuth2::Version with VersionGem helpers to provide semantic version helpers.
|
|
38
101
|
OAuth2::Version.class_eval do
|
|
39
102
|
extend VersionGem::Basic
|
|
40
103
|
end
|