oauth2 1.4.9 → 2.0.5

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.
@@ -1,27 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'multi_json'
3
+ require 'json'
4
4
  require 'multi_xml'
5
5
  require 'rack'
6
6
 
7
7
  module OAuth2
8
8
  # OAuth2::Response class
9
9
  class Response
10
+ DEFAULT_OPTIONS = {
11
+ parse: :automatic,
12
+ snaky: true,
13
+ }.freeze
10
14
  attr_reader :response
11
- attr_accessor :error, :options
15
+ attr_accessor :options
12
16
 
13
17
  # Procs that, when called, will parse a response body according
14
18
  # to the specified format.
15
19
  @@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 },
20
+ query: ->(body) { Rack::Utils.parse_query(body) },
21
+ text: ->(body) { body },
19
22
  }
20
23
 
21
24
  # Content type assignments for various potential HTTP content types.
22
25
  @@content_types = {
23
- 'application/json' => :json,
24
- 'text/javascript' => :json,
25
26
  'application/x-www-form-urlencoded' => :query,
26
27
  'text/plain' => :text,
27
28
  }
@@ -42,12 +43,17 @@ module OAuth2
42
43
  # Initializes a Response instance
43
44
  #
44
45
  # @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),
46
+ # @param [Symbol] parse (:automatic) how to parse the response body. one of :query (for x-www-form-urlencoded),
47
47
  # :json, or :automatic (determined by Content-Type response header)
48
- def initialize(response, opts = {})
48
+ # @param [true, false] snaky (true) Convert @parsed to a snake-case,
49
+ # indifferent-access OAuth2::SnakyHash, which is a subclass of Hashie::Mash::Rash (from rash_alt gem)?
50
+ # @param [Hash] options all other options for initializing the instance
51
+ def initialize(response, parse: :automatic, snaky: true, **options)
49
52
  @response = response
50
- @options = {:parse => :automatic}.merge(opts)
53
+ @options = {
54
+ parse: parse,
55
+ snaky: snaky,
56
+ }.merge(options)
51
57
  end
52
58
 
53
59
  # The HTTP response headers
@@ -65,29 +71,78 @@ module OAuth2
65
71
  response.body || ''
66
72
  end
67
73
 
68
- # The parsed response body.
69
- # Will attempt to parse application/x-www-form-urlencoded and
70
- # application/json Content-Type response bodies
74
+ # The {#response} {#body} as parsed by {#parser}.
75
+ #
76
+ # @return [Object] As returned by {#parser} if it is #call-able.
77
+ # @return [nil] If the {#parser} is not #call-able.
71
78
  def parsed
72
- return nil unless @@parsers.key?(parser)
79
+ return @parsed if defined?(@parsed)
80
+
81
+ @parsed =
82
+ if parser.respond_to?(:call)
83
+ case parser.arity
84
+ when 0
85
+ parser.call
86
+ when 1
87
+ parser.call(body)
88
+ else
89
+ parser.call(body, response)
90
+ end
91
+ end
73
92
 
74
- @parsed ||= @@parsers[parser].call(body)
93
+ @parsed = OAuth2::SnakyHash.new(@parsed) if options[:snaky] && @parsed.is_a?(Hash)
94
+
95
+ @parsed
75
96
  end
76
97
 
77
98
  # Attempts to determine the content type of the response.
78
99
  def content_type
79
- ((response.headers.values_at('content-type', 'Content-Type').compact.first || '').split(';').first || '').strip
100
+ return nil unless response.headers
101
+
102
+ ((response.headers.values_at('content-type', 'Content-Type').compact.first || '').split(';').first || '').strip.downcase
80
103
  end
81
104
 
82
- # Determines the parser that will be used to supply the content of #parsed
105
+ # Determines the parser (a Proc or other Object which responds to #call)
106
+ # that will be passed the {#body} (and optional {#response}) to supply
107
+ # {#parsed}.
108
+ #
109
+ # The parser can be supplied as the +:parse+ option in the form of a Proc
110
+ # (or other Object responding to #call) or a Symbol. In the latter case,
111
+ # the actual parser will be looked up in {@@parsers} by the supplied Symbol.
112
+ #
113
+ # If no +:parse+ option is supplied, the lookup Symbol will be determined
114
+ # by looking up {#content_type} in {@@content_types}.
115
+ #
116
+ # If {#parser} is a Proc, it will be called with no arguments, just
117
+ # {#body}, or {#body} and {#response}, depending on the Proc's arity.
118
+ #
119
+ # @return [Proc, #call] If a parser was found.
120
+ # @return [nil] If no parser was found.
83
121
  def parser
84
- return options[:parse].to_sym if @@parsers.key?(options[:parse])
122
+ return @parser if defined?(@parser)
123
+
124
+ @parser =
125
+ if options[:parse].respond_to?(:call)
126
+ options[:parse]
127
+ elsif options[:parse]
128
+ @@parsers[options[:parse].to_sym]
129
+ end
85
130
 
86
- @@content_types[content_type]
131
+ @parser ||= @@parsers[@@content_types[content_type]]
87
132
  end
88
133
  end
89
134
  end
90
135
 
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
136
+ OAuth2::Response.register_parser(:xml, ['text/xml', 'application/rss+xml', 'application/rdf+xml', 'application/atom+xml', 'application/xml']) do |body|
137
+ next body unless body.respond_to?(:to_str)
138
+
139
+ MultiXml.parse(body)
140
+ end
141
+
142
+ 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
+ next body unless body.respond_to?(:to_str)
144
+
145
+ body = body.dup.force_encoding(::Encoding::ASCII_8BIT) if body.respond_to?(:force_encoding)
146
+
147
+ ::JSON.parse(body)
93
148
  end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OAuth2
4
+ # Hash which allow assign string key in camel case
5
+ # and query on both camel and snake case
6
+ class SnakyHash < ::Hashie::Mash::Rash
7
+ end
8
+ end
@@ -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
  #
@@ -32,45 +39,63 @@ module OAuth2
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
37
63
  #
38
- # params :hmac_secret, secret string.
39
- # params :private_key, private key string.
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
40
67
  #
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
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')})
45
71
  #
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))
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
75
+ #
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
+ JWT.encode(claims, encoding_opts[:key], encoding_opts[:algorithm])
74
99
  end
75
100
  end
76
101
  end
@@ -17,6 +17,7 @@ module OAuth2
17
17
  #
18
18
  # @param [Hash] params additional query parameters for the URL
19
19
  def authorize_url(params = {})
20
+ assert_valid_params(params)
20
21
  @client.authorize_url(authorize_params.merge(params))
21
22
  end
22
23
 
@@ -24,12 +25,22 @@ module OAuth2
24
25
  #
25
26
  # @param [String] code The Authorization Code value
26
27
  # @param [Hash] params additional params
27
- # @param [Hash] opts options
28
+ # @param [Hash] opts access_token_opts, @see Client#get_token
28
29
  # @note that you must also provide a :redirect_uri with most OAuth 2.0 providers
29
30
  def get_token(code, params = {}, opts = {})
30
31
  params = {'grant_type' => 'authorization_code', 'code' => code}.merge(@client.redirection_params).merge(params)
32
+ params_dup = params.dup
33
+ params.each_key do |key|
34
+ params_dup[key.to_s] = params_dup.delete(key) if key.is_a?(Symbol)
35
+ end
31
36
 
32
- @client.get_token(params, opts)
37
+ @client.get_token(params_dup, opts)
38
+ end
39
+
40
+ private
41
+
42
+ def assert_valid_params(params)
43
+ raise(ArgumentError, 'client_secret is not allowed in authorize URL query params') if params.key?(:client_secret) || params.key?('client_secret')
33
44
  end
34
45
  end
35
46
  end
@@ -19,7 +19,7 @@ module OAuth2
19
19
  # @param [Hash] opts options
20
20
  def get_token(params = {}, opts = {})
21
21
  params = params.merge('grant_type' => 'client_credentials')
22
- @client.get_token(params, opts.merge('refresh_token' => nil))
22
+ @client.get_token(params, opts)
23
23
  end
24
24
  end
25
25
  end
@@ -17,6 +17,7 @@ module OAuth2
17
17
  #
18
18
  # @param [Hash] params additional query parameters for the URL
19
19
  def authorize_url(params = {})
20
+ assert_valid_params(params)
20
21
  @client.authorize_url(authorize_params.merge(params))
21
22
  end
22
23
 
@@ -26,6 +27,12 @@ module OAuth2
26
27
  def get_token(*)
27
28
  raise(NotImplementedError, 'The token is accessed differently in this strategy')
28
29
  end
30
+
31
+ private
32
+
33
+ def assert_valid_params(params)
34
+ raise(ArgumentError, 'client_secret is not allowed in authorize URL query params') if params.key?(:client_secret) || params.key?('client_secret')
35
+ end
29
36
  end
30
37
  end
31
38
  end
@@ -2,64 +2,6 @@
2
2
 
3
3
  module OAuth2
4
4
  module Version
5
- VERSION = to_s
6
-
7
- module_function
8
-
9
- # The major version
10
- #
11
- # @return [Integer]
12
- def major
13
- 1
14
- end
15
-
16
- # The minor version
17
- #
18
- # @return [Integer]
19
- def minor
20
- 4
21
- end
22
-
23
- # The patch version
24
- #
25
- # @return [Integer]
26
- def patch
27
- 9
28
- end
29
-
30
- # The pre-release version, if any
31
- #
32
- # @return [String, NilClass]
33
- def pre
34
- nil
35
- end
36
-
37
- # The version number as a hash
38
- #
39
- # @return [Hash]
40
- def to_h
41
- {
42
- :major => major,
43
- :minor => minor,
44
- :patch => patch,
45
- :pre => pre,
46
- }
47
- end
48
-
49
- # The version number as an array
50
- #
51
- # @return [Array]
52
- def to_a
53
- [major, minor, patch, pre].compact
54
- end
55
-
56
- # The version number as a string
57
- #
58
- # @return [String]
59
- def to_s
60
- v = [major, minor, patch].compact.join('.')
61
- v += "-#{pre}" if pre
62
- v
63
- end
5
+ VERSION = '2.0.5'.freeze
64
6
  end
65
7
  end
data/lib/oauth2.rb CHANGED
@@ -1,6 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # includes modules from stdlib
4
+ require 'cgi'
5
+ require 'time'
6
+
7
+ # third party gems
8
+ require 'rash'
9
+ require 'version_gem'
10
+
11
+ # includes gem files
12
+ require 'oauth2/version'
3
13
  require 'oauth2/error'
14
+ require 'oauth2/snaky_hash'
4
15
  require 'oauth2/authenticator'
5
16
  require 'oauth2/client'
6
17
  require 'oauth2/strategy/base'
@@ -10,5 +21,12 @@ require 'oauth2/strategy/password'
10
21
  require 'oauth2/strategy/client_credentials'
11
22
  require 'oauth2/strategy/assertion'
12
23
  require 'oauth2/access_token'
13
- require 'oauth2/mac_token'
14
24
  require 'oauth2/response'
25
+
26
+ # The namespace of this library
27
+ module OAuth2
28
+ end
29
+
30
+ OAuth2::Version.class_eval do
31
+ extend VersionGem::Basic
32
+ end