oauth2 1.4.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 +706 -88
- data/CITATION.cff +20 -0
- data/CODE_OF_CONDUCT.md +24 -23
- data/CONTRIBUTING.md +229 -0
- data/FUNDING.md +77 -0
- data/{LICENSE → LICENSE.txt} +2 -2
- data/OIDC.md +158 -0
- data/README.md +1513 -251
- data/REEK +0 -0
- data/RUBOCOP.md +71 -0
- data/SECURITY.md +21 -0
- data/lib/oauth2/access_token.rb +276 -39
- data/lib/oauth2/authenticator.rb +45 -8
- data/lib/oauth2/client.rb +406 -129
- data/lib/oauth2/error.rb +59 -24
- data/lib/oauth2/filtered_attributes.rb +52 -0
- data/lib/oauth2/response.rb +127 -36
- data/lib/oauth2/strategy/assertion.rb +68 -40
- data/lib/oauth2/strategy/auth_code.rb +25 -4
- data/lib/oauth2/strategy/client_credentials.rb +3 -3
- data/lib/oauth2/strategy/implicit.rb +17 -2
- data/lib/oauth2/strategy/password.rb +14 -4
- data/lib/oauth2/version.rb +1 -59
- data/lib/oauth2.rb +79 -12
- 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 +336 -89
- metadata.gz.sig +0 -0
- data/lib/oauth2/mac_token.rb +0 -130
- data/spec/fixtures/README.md +0 -11
- data/spec/fixtures/RS256/jwtRS256.key +0 -51
- data/spec/fixtures/RS256/jwtRS256.key.pub +0 -14
- data/spec/helper.rb +0 -33
- data/spec/oauth2/access_token_spec.rb +0 -218
- data/spec/oauth2/authenticator_spec.rb +0 -86
- data/spec/oauth2/client_spec.rb +0 -556
- data/spec/oauth2/mac_token_spec.rb +0 -122
- data/spec/oauth2/response_spec.rb +0 -96
- data/spec/oauth2/strategy/assertion_spec.rb +0 -113
- data/spec/oauth2/strategy/auth_code_spec.rb +0 -108
- data/spec/oauth2/strategy/base_spec.rb +0 -7
- data/spec/oauth2/strategy/client_credentials_spec.rb +0 -71
- data/spec/oauth2/strategy/implicit_spec.rb +0 -28
- data/spec/oauth2/strategy/password_spec.rb +0 -58
- data/spec/oauth2/version_spec.rb +0 -23
data/lib/oauth2/error.rb
CHANGED
@@ -1,42 +1,77 @@
|
|
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
|
-
|
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
|
-
#
|
8
|
-
#
|
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
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
20
|
+
if response.respond_to?(:parsed)
|
21
|
+
if response.parsed.is_a?(Hash)
|
22
|
+
@code = response.parsed["error"]
|
23
|
+
@description = response.parsed["error_description"]
|
24
|
+
end
|
25
|
+
elsif response.is_a?(Hash)
|
26
|
+
@code = response["error"]
|
27
|
+
@description = response["error_description"]
|
17
28
|
end
|
18
|
-
|
19
|
-
|
29
|
+
@body = if response.respond_to?(:body)
|
30
|
+
response.body
|
31
|
+
else
|
32
|
+
@response
|
33
|
+
end
|
34
|
+
message_opts = parse_error_description(@code, @description)
|
35
|
+
super(error_message(@body, message_opts))
|
20
36
|
end
|
21
37
|
|
22
|
-
|
23
|
-
|
24
|
-
#
|
38
|
+
private
|
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
|
25
45
|
def error_message(response_body, opts = {})
|
26
|
-
|
46
|
+
lines = []
|
47
|
+
|
48
|
+
lines << opts[:error_description] if opts[:error_description]
|
27
49
|
|
28
|
-
|
50
|
+
error_string = if response_body.respond_to?(:encode) && opts[:error_description].respond_to?(:encoding)
|
51
|
+
script_encoding = opts[:error_description].encoding
|
52
|
+
response_body.encode(script_encoding, invalid: :replace, undef: :replace)
|
53
|
+
else
|
54
|
+
response_body
|
55
|
+
end
|
56
|
+
|
57
|
+
lines << error_string
|
58
|
+
|
59
|
+
lines.join("\n")
|
60
|
+
end
|
29
61
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
67
|
+
def parse_error_description(code, description)
|
68
|
+
return {} unless code || description
|
36
69
|
|
37
|
-
|
70
|
+
error_description = ""
|
71
|
+
error_description += "#{code}: " if code
|
72
|
+
error_description += description if description
|
38
73
|
|
39
|
-
|
74
|
+
{error_description: error_description}
|
40
75
|
end
|
41
76
|
end
|
42
77
|
end
|
@@ -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,36 +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
|
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
|
-
#
|
14
|
-
|
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
|
-
:
|
17
|
-
:
|
18
|
-
:text => lambda { |body| body },
|
33
|
+
query: ->(body) { Rack::Utils.parse_query(body) },
|
34
|
+
text: ->(body) { body },
|
19
35
|
}
|
20
36
|
|
21
|
-
#
|
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
|
-
|
24
|
-
|
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
46
|
# Adds a new content type parser.
|
30
47
|
#
|
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]
|
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]
|
34
53
|
def self.register_parser(key, mime_types, &block)
|
35
54
|
key = key.to_sym
|
36
55
|
@@parsers[key] = block
|
@@ -42,52 +61,124 @@ module OAuth2
|
|
42
61
|
# Initializes a Response instance
|
43
62
|
#
|
44
63
|
# @param [Faraday::Response] response The Faraday response instance
|
45
|
-
# @param [
|
46
|
-
# @
|
47
|
-
#
|
48
|
-
|
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)
|
49
73
|
@response = response
|
50
|
-
@options = {
|
74
|
+
@options = {
|
75
|
+
parse: parse,
|
76
|
+
snaky: snaky,
|
77
|
+
snaky_hash_klass: snaky_hash_klass,
|
78
|
+
}.merge(options)
|
51
79
|
end
|
52
80
|
|
53
81
|
# The HTTP response headers
|
82
|
+
#
|
83
|
+
# @return [Hash] The response headers
|
54
84
|
def headers
|
55
85
|
response.headers
|
56
86
|
end
|
57
87
|
|
58
88
|
# The HTTP response status code
|
89
|
+
#
|
90
|
+
# @return [Integer] The response status code
|
59
91
|
def status
|
60
92
|
response.status
|
61
93
|
end
|
62
94
|
|
63
95
|
# The HTTP response body
|
96
|
+
#
|
97
|
+
# @return [String] The response body or empty string if nil
|
64
98
|
def body
|
65
|
-
response.body ||
|
99
|
+
response.body || ""
|
66
100
|
end
|
67
101
|
|
68
|
-
# The parsed response body
|
69
|
-
#
|
70
|
-
#
|
102
|
+
# The parsed response body
|
103
|
+
#
|
104
|
+
# @return [Object, SnakyHash::StringKeyed] The parsed response body
|
105
|
+
# @return [nil] If no parser is available
|
71
106
|
def parsed
|
72
|
-
return
|
107
|
+
return @parsed if defined?(@parsed)
|
108
|
+
|
109
|
+
@parsed =
|
110
|
+
if parser.respond_to?(:call)
|
111
|
+
case parser.arity
|
112
|
+
when 0
|
113
|
+
parser.call
|
114
|
+
when 1
|
115
|
+
parser.call(body)
|
116
|
+
else
|
117
|
+
parser.call(body, response)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
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
|
73
125
|
|
74
|
-
@parsed
|
126
|
+
@parsed
|
75
127
|
end
|
76
128
|
|
77
|
-
#
|
129
|
+
# Determines the content type of the response
|
130
|
+
#
|
131
|
+
# @return [String, nil] The content type or nil if headers are not present
|
78
132
|
def content_type
|
79
|
-
|
133
|
+
return unless response.headers
|
134
|
+
|
135
|
+
((response.headers.values_at("content-type", "Content-Type").compact.first || "").split(";").first || "").strip.downcase
|
80
136
|
end
|
81
137
|
|
82
|
-
# Determines the parser
|
138
|
+
# Determines the parser to be used for the response body
|
139
|
+
#
|
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.
|
143
|
+
#
|
144
|
+
# @note If no +:parse+ option is supplied, the lookup Symbol will be determined
|
145
|
+
# by looking up {#content_type} in {@@content_types}.
|
146
|
+
#
|
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.
|
149
|
+
#
|
150
|
+
# @return [Proc, #call] The parser proc or callable object
|
151
|
+
# @return [nil] If no suitable parser is found
|
83
152
|
def parser
|
84
|
-
return
|
153
|
+
return @parser if defined?(@parser)
|
85
154
|
|
86
|
-
|
155
|
+
@parser =
|
156
|
+
if options[:parse].respond_to?(:call)
|
157
|
+
options[:parse]
|
158
|
+
elsif options[:parse]
|
159
|
+
@@parsers[options[:parse].to_sym]
|
160
|
+
end
|
161
|
+
|
162
|
+
@parser ||= @@parsers[@@content_types[content_type]]
|
87
163
|
end
|
88
164
|
end
|
89
165
|
end
|
90
166
|
|
91
|
-
|
92
|
-
|
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|
|
170
|
+
next body unless body.respond_to?(:to_str)
|
171
|
+
|
172
|
+
MultiXml.parse(body)
|
173
|
+
end
|
174
|
+
|
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|
|
178
|
+
next body unless body.respond_to?(:to_str)
|
179
|
+
|
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?
|
182
|
+
|
183
|
+
JSON.parse(body)
|
93
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
|
@@ -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
|
-
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
#
|
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
|
-
#
|
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,
|
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]
|
36
|
-
#
|
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
|
-
#
|
39
|
-
#
|
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
|
-
#
|
42
|
-
#
|
43
|
-
#
|
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]
|
47
|
-
|
48
|
-
|
49
|
-
|
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
|
-
|
53
|
-
|
86
|
+
private
|
87
|
+
|
88
|
+
def build_request(assertion, request_opts = {})
|
54
89
|
{
|
55
|
-
:
|
56
|
-
:
|
57
|
-
|
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(
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
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(
|
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
|
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 = {
|
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
|
-
|
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,
|
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(
|
22
|
-
@client.get_token(params, opts
|
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(
|
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,
|
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
|