oauth2 1.4.7 → 2.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,4 +1,6 @@
1
- require 'multi_json'
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
2
4
  require 'multi_xml'
3
5
  require 'rack'
4
6
 
@@ -6,20 +8,17 @@ module OAuth2
6
8
  # OAuth2::Response class
7
9
  class Response
8
10
  attr_reader :response
9
- attr_accessor :error, :options
11
+ attr_accessor :options
10
12
 
11
13
  # Procs that, when called, will parse a response body according
12
14
  # to the specified format.
13
15
  @@parsers = {
14
- :json => lambda { |body| MultiJson.load(body) rescue body }, # rubocop:disable Style/RescueModifier
15
- :query => lambda { |body| Rack::Utils.parse_query(body) },
16
- :text => lambda { |body| body },
16
+ query: ->(body) { Rack::Utils.parse_query(body) },
17
+ text: ->(body) { body },
17
18
  }
18
19
 
19
20
  # Content type assignments for various potential HTTP content types.
20
21
  @@content_types = {
21
- 'application/json' => :json,
22
- 'text/javascript' => :json,
23
22
  'application/x-www-form-urlencoded' => :query,
24
23
  'text/plain' => :text,
25
24
  }
@@ -40,12 +39,17 @@ module OAuth2
40
39
  # Initializes a Response instance
41
40
  #
42
41
  # @param [Faraday::Response] response The Faraday response instance
43
- # @param [Hash] opts options in which to initialize the instance
44
- # @option opts [Symbol] :parse (:automatic) how to parse the response body. one of :query (for x-www-form-urlencoded),
42
+ # @param [Symbol] parse (:automatic) how to parse the response body. one of :query (for x-www-form-urlencoded),
45
43
  # :json, or :automatic (determined by Content-Type response header)
46
- def initialize(response, opts = {})
44
+ # @param [true, false] snaky (true) Convert @parsed to a snake-case,
45
+ # indifferent-access OAuth2::SnakyHash, which is a subclass of Hashie::Mash::Rash (from rash_alt gem)?
46
+ # @param [Hash] options all other options for initializing the instance
47
+ def initialize(response, parse: :automatic, snaky: true, **options)
47
48
  @response = response
48
- @options = {:parse => :automatic}.merge(opts)
49
+ @options = {
50
+ parse: parse,
51
+ snaky: snaky,
52
+ }.merge(options)
49
53
  end
50
54
 
51
55
  # The HTTP response headers
@@ -63,29 +67,78 @@ module OAuth2
63
67
  response.body || ''
64
68
  end
65
69
 
66
- # The parsed response body.
67
- # Will attempt to parse application/x-www-form-urlencoded and
68
- # application/json Content-Type response bodies
70
+ # The {#response} {#body} as parsed by {#parser}.
71
+ #
72
+ # @return [Object] As returned by {#parser} if it is #call-able.
73
+ # @return [nil] If the {#parser} is not #call-able.
69
74
  def parsed
70
- return nil unless @@parsers.key?(parser)
75
+ return @parsed if defined?(@parsed)
76
+
77
+ @parsed =
78
+ if parser.respond_to?(:call)
79
+ case parser.arity
80
+ when 0
81
+ parser.call
82
+ when 1
83
+ parser.call(body)
84
+ else
85
+ parser.call(body, response)
86
+ end
87
+ end
88
+
89
+ @parsed = OAuth2::SnakyHash.new(@parsed) if options[:snaky] && @parsed.is_a?(Hash)
71
90
 
72
- @parsed ||= @@parsers[parser].call(body)
91
+ @parsed
73
92
  end
74
93
 
75
94
  # Attempts to determine the content type of the response.
76
95
  def content_type
77
- ((response.headers.values_at('content-type', 'Content-Type').compact.first || '').split(';').first || '').strip
96
+ return nil unless response.headers
97
+
98
+ ((response.headers.values_at('content-type', 'Content-Type').compact.first || '').split(';').first || '').strip.downcase
78
99
  end
79
100
 
80
- # Determines the parser that will be used to supply the content of #parsed
101
+ # Determines the parser (a Proc or other Object which responds to #call)
102
+ # that will be passed the {#body} (and optional {#response}) to supply
103
+ # {#parsed}.
104
+ #
105
+ # The parser can be supplied as the +:parse+ option in the form of a Proc
106
+ # (or other Object responding to #call) or a Symbol. In the latter case,
107
+ # the actual parser will be looked up in {@@parsers} by the supplied Symbol.
108
+ #
109
+ # If no +:parse+ option is supplied, the lookup Symbol will be determined
110
+ # by looking up {#content_type} in {@@content_types}.
111
+ #
112
+ # If {#parser} is a Proc, it will be called with no arguments, just
113
+ # {#body}, or {#body} and {#response}, depending on the Proc's arity.
114
+ #
115
+ # @return [Proc, #call] If a parser was found.
116
+ # @return [nil] If no parser was found.
81
117
  def parser
82
- return options[:parse].to_sym if @@parsers.key?(options[:parse])
118
+ return @parser if defined?(@parser)
119
+
120
+ @parser =
121
+ if options[:parse].respond_to?(:call)
122
+ options[:parse]
123
+ elsif options[:parse]
124
+ @@parsers[options[:parse].to_sym]
125
+ end
83
126
 
84
- @@content_types[content_type]
127
+ @parser ||= @@parsers[@@content_types[content_type]]
85
128
  end
86
129
  end
87
130
  end
88
131
 
89
- OAuth2::Response.register_parser(:xml, ['text/xml', 'application/rss+xml', 'application/rdf+xml', 'application/atom+xml']) do |body|
90
- MultiXml.parse(body) rescue body # rubocop:disable Style/RescueModifier
132
+ OAuth2::Response.register_parser(:xml, ['text/xml', 'application/rss+xml', 'application/rdf+xml', 'application/atom+xml', 'application/xml']) do |body|
133
+ next body unless body.respond_to?(:to_str)
134
+
135
+ MultiXml.parse(body)
136
+ end
137
+
138
+ 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|
139
+ next body unless body.respond_to?(:to_str)
140
+
141
+ body = body.dup.force_encoding(::Encoding::ASCII_8BIT) if body.respond_to?(:force_encoding)
142
+
143
+ ::JSON.parse(body)
91
144
  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
@@ -1,22 +1,31 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'jwt'
2
4
 
3
5
  module OAuth2
4
6
  module Strategy
5
7
  # The Client Assertion Strategy
6
8
  #
7
- # @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-4.1.3
9
+ # @see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-10#section-4.1.3
8
10
  #
9
11
  # Sample usage:
10
12
  # client = OAuth2::Client.new(client_id, client_secret,
11
- # :site => 'http://localhost:8080')
13
+ # :site => 'http://localhost:8080',
14
+ # :auth_scheme => :request_body)
15
+ #
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
+ # }
12
22
  #
13
- # params = {:hmac_secret => "some secret",
14
- # # or :private_key => "private key string",
15
- # :iss => "http://localhost:3001",
16
- # :prn => "me@here.com",
17
- # :exp => Time.now.utc.to_i + 3600}
23
+ # encoding = {
24
+ # :algorithm => 'HS256',
25
+ # :key => 'secret_key',
26
+ # }
18
27
  #
19
- # access = client.assertion.get_token(params)
28
+ # access = client.assertion.get_token(claim_set, encoding)
20
29
  # access.token # actual access_token string
21
30
  # access.get("/api/stuff") # making api calls with access token in header
22
31
  #
@@ -30,45 +39,63 @@ module OAuth2
30
39
 
31
40
  # Retrieve an access token given the specified client.
32
41
  #
33
- # @param [Hash] params assertion params
34
- # 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
35
63
  #
36
- # params :hmac_secret, secret string.
37
- # 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
38
67
  #
39
- # params :iss, issuer
40
- # params :aud, audience, optional
41
- # params :prn, principal, current user
42
- # 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')})
43
71
  #
44
- # @param [Hash] opts options
45
- def get_token(params = {}, opts = {})
46
- hash = build_request(params)
47
- @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.merge('refresh_token' => nil))
48
84
  end
49
85
 
50
- def build_request(params)
51
- assertion = build_assertion(params)
86
+ private
87
+
88
+ def build_request(assertion, request_opts = {})
52
89
  {
53
- :grant_type => 'assertion',
54
- :assertion_type => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
55
- :assertion => assertion,
56
- :scope => params[:scope],
57
- }
90
+ grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
91
+ assertion: assertion,
92
+ }.merge(request_opts)
58
93
  end
59
94
 
60
- def build_assertion(params)
61
- claims = {
62
- :iss => params[:iss],
63
- :aud => params[:aud],
64
- :prn => params[:prn],
65
- :exp => params[:exp],
66
- }
67
- if params[:hmac_secret]
68
- JWT.encode(claims, params[:hmac_secret], 'HS256')
69
- elsif params[:private_key]
70
- JWT.encode(claims, params[:private_key], 'RS256')
71
- 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])
72
99
  end
73
100
  end
74
101
  end
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module OAuth2
2
4
  module Strategy
3
5
  # The Authorization Code Strategy
4
6
  #
5
- # @see http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.1
7
+ # @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-15#section-4.1
6
8
  class AuthCode < Base
7
9
  # The required query parameters for the authorize URL
8
10
  #
@@ -15,6 +17,7 @@ module OAuth2
15
17
  #
16
18
  # @param [Hash] params additional query parameters for the URL
17
19
  def authorize_url(params = {})
20
+ assert_valid_params(params)
18
21
  @client.authorize_url(authorize_params.merge(params))
19
22
  end
20
23
 
@@ -26,8 +29,18 @@ module OAuth2
26
29
  # @note that you must also provide a :redirect_uri with most OAuth 2.0 providers
27
30
  def get_token(code, params = {}, opts = {})
28
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
36
+
37
+ @client.get_token(params_dup, opts)
38
+ end
39
+
40
+ private
29
41
 
30
- @client.get_token(params, opts)
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')
31
44
  end
32
45
  end
33
46
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module OAuth2
2
4
  module Strategy
3
5
  class Base
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module OAuth2
2
4
  module Strategy
3
5
  # The Client Credentials Strategy
4
6
  #
5
- # @see http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.4
7
+ # @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-15#section-4.4
6
8
  class ClientCredentials < Base
7
9
  # Not used for this strategy
8
10
  #
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module OAuth2
2
4
  module Strategy
3
5
  # The Implicit Strategy
4
6
  #
5
- # @see http://tools.ietf.org/html/draft-ietf-oauth-v2-26#section-4.2
7
+ # @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-26#section-4.2
6
8
  class Implicit < Base
7
9
  # The required query parameters for the authorize URL
8
10
  #
@@ -15,6 +17,7 @@ module OAuth2
15
17
  #
16
18
  # @param [Hash] params additional query parameters for the URL
17
19
  def authorize_url(params = {})
20
+ assert_valid_params(params)
18
21
  @client.authorize_url(authorize_params.merge(params))
19
22
  end
20
23
 
@@ -24,6 +27,12 @@ module OAuth2
24
27
  def get_token(*)
25
28
  raise(NotImplementedError, 'The token is accessed differently in this strategy')
26
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
27
36
  end
28
37
  end
29
38
  end
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module OAuth2
2
4
  module Strategy
3
5
  # The Resource Owner Password Credentials Authorization Strategy
4
6
  #
5
- # @see http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.3
7
+ # @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-15#section-4.3
6
8
  class Password < Base
7
9
  # Not used for this strategy
8
10
  #
@@ -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
- 7
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.3'.freeze
64
6
  end
65
7
  end
data/lib/oauth2.rb CHANGED
@@ -1,4 +1,17 @@
1
+ # frozen_string_literal: true
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'
1
13
  require 'oauth2/error'
14
+ require 'oauth2/snaky_hash'
2
15
  require 'oauth2/authenticator'
3
16
  require 'oauth2/client'
4
17
  require 'oauth2/strategy/base'
@@ -8,5 +21,12 @@ require 'oauth2/strategy/password'
8
21
  require 'oauth2/strategy/client_credentials'
9
22
  require 'oauth2/strategy/assertion'
10
23
  require 'oauth2/access_token'
11
- require 'oauth2/mac_token'
12
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