oauth2 1.4.0 → 2.0.0

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,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 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
  }
@@ -45,7 +44,7 @@ module OAuth2
45
44
  # :json, or :automatic (determined by Content-Type response header)
46
45
  def initialize(response, opts = {})
47
46
  @response = response
48
- @options = {:parse => :automatic}.merge(opts)
47
+ @options = {parse: :automatic}.merge(opts)
49
48
  end
50
49
 
51
50
  # The HTTP response headers
@@ -63,27 +62,74 @@ module OAuth2
63
62
  response.body || ''
64
63
  end
65
64
 
66
- # The parsed response body.
67
- # Will attempt to parse application/x-www-form-urlencoded and
68
- # application/json Content-Type response bodies
65
+ # The {#response} {#body} as parsed by {#parser}.
66
+ #
67
+ # @return [Object] As returned by {#parser} if it is #call-able.
68
+ # @return [nil] If the {#parser} is not #call-able.
69
69
  def parsed
70
- return nil unless @@parsers.key?(parser)
71
- @parsed ||= @@parsers[parser].call(body)
70
+ return @parsed if defined?(@parsed)
71
+
72
+ @parsed =
73
+ if parser.respond_to?(:call)
74
+ case parser.arity
75
+ when 0
76
+ parser.call
77
+ when 1
78
+ parser.call(body)
79
+ else
80
+ parser.call(body, response)
81
+ end
82
+ end
83
+
84
+ @parsed = OAuth2::SnakyHash.new(@parsed) if @parsed.is_a?(Hash)
85
+
86
+ @parsed
72
87
  end
73
88
 
74
89
  # Attempts to determine the content type of the response.
75
90
  def content_type
76
- ((response.headers.values_at('content-type', 'Content-Type').compact.first || '').split(';').first || '').strip
91
+ return nil unless response.headers
92
+
93
+ ((response.headers.values_at('content-type', 'Content-Type').compact.first || '').split(';').first || '').strip.downcase
77
94
  end
78
95
 
79
- # Determines the parser that will be used to supply the content of #parsed
96
+ # Determines the parser (a Proc or other Object which responds to #call)
97
+ # that will be passed the {#body} (and optional {#response}) to supply
98
+ # {#parsed}.
99
+ #
100
+ # The parser can be supplied as the +:parse+ option in the form of a Proc
101
+ # (or other Object responding to #call) or a Symbol. In the latter case,
102
+ # the actual parser will be looked up in {@@parsers} by the supplied Symbol.
103
+ #
104
+ # If no +:parse+ option is supplied, the lookup Symbol will be determined
105
+ # by looking up {#content_type} in {@@content_types}.
106
+ #
107
+ # If {#parser} is a Proc, it will be called with no arguments, just
108
+ # {#body}, or {#body} and {#response}, depending on the Proc's arity.
109
+ #
110
+ # @return [Proc, #call] If a parser was found.
111
+ # @return [nil] If no parser was found.
80
112
  def parser
81
- return options[:parse].to_sym if @@parsers.key?(options[:parse])
82
- @@content_types[content_type]
113
+ return @parser if defined?(@parser)
114
+
115
+ @parser =
116
+ if options[:parse].respond_to?(:call)
117
+ options[:parse]
118
+ elsif options[:parse]
119
+ @@parsers[options[:parse].to_sym]
120
+ end
121
+
122
+ @parser ||= @@parsers[@@content_types[content_type]]
83
123
  end
84
124
  end
85
125
  end
86
126
 
87
- OAuth2::Response.register_parser(:xml, ['text/xml', 'application/rss+xml', 'application/rdf+xml', 'application/atom+xml']) do |body|
88
- MultiXml.parse(body) rescue body # rubocop:disable RescueModifier
127
+ OAuth2::Response.register_parser(:xml, ['text/xml', 'application/rss+xml', 'application/rdf+xml', 'application/atom+xml', 'application/xml']) do |body|
128
+ MultiXml.parse(body)
129
+ end
130
+
131
+ 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|
132
+ body = body.dup.force_encoding(::Encoding::ASCII_8BIT) if body.respond_to?(:force_encoding)
133
+
134
+ ::JSON.parse(body)
89
135
  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
  #
@@ -18,8 +20,8 @@ module OAuth2
18
20
  # @param [Hash] params additional params
19
21
  def get_token(username, password, params = {}, opts = {})
20
22
  params = {'grant_type' => 'password',
21
- 'username' => username,
22
- 'password' => password}.merge(params)
23
+ 'username' => username,
24
+ 'password' => password}.merge(params)
23
25
  @client.get_token(params, opts)
24
26
  end
25
27
  end
@@ -1,59 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module OAuth2
2
4
  module Version
3
- module_function
4
-
5
- # The major version
6
- #
7
- # @return [Integer]
8
- def major
9
- 1
10
- end
11
-
12
- # The minor version
13
- #
14
- # @return [Integer]
15
- def minor
16
- 4
17
- end
18
-
19
- # The patch version
20
- #
21
- # @return [Integer]
22
- def patch
23
- 0
24
- end
25
-
26
- # The pre-release version, if any
27
- #
28
- # @return [Integer, NilClass]
29
- def pre
30
- nil
31
- end
32
-
33
- # The version number as a hash
34
- #
35
- # @return [Hash]
36
- def to_h
37
- {
38
- :major => major,
39
- :minor => minor,
40
- :patch => patch,
41
- :pre => pre,
42
- }
43
- end
44
-
45
- # The version number as an array
46
- #
47
- # @return [Array]
48
- def to_a
49
- [major, minor, patch, pre].compact
50
- end
51
-
52
- # The version number as a string
53
- #
54
- # @return [String]
55
- def to_s
56
- to_a.join('.')
57
- end
5
+ VERSION = '2.0.0'.freeze
58
6
  end
59
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