oauth2 1.4.2 → 1.4.7

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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +28 -2
  3. data/CODE_OF_CONDUCT.md +105 -46
  4. data/README.md +33 -5
  5. data/lib/oauth2/access_token.rb +12 -4
  6. data/lib/oauth2/authenticator.rb +10 -0
  7. data/lib/oauth2/client.rb +63 -16
  8. data/lib/oauth2/mac_token.rb +10 -2
  9. data/lib/oauth2/response.rb +5 -3
  10. data/lib/oauth2/strategy/assertion.rb +3 -3
  11. data/lib/oauth2/strategy/password.rb +2 -2
  12. data/lib/oauth2/version.rb +9 -3
  13. data/spec/helper.rb +37 -0
  14. data/spec/oauth2/access_token_spec.rb +216 -0
  15. data/spec/oauth2/authenticator_spec.rb +84 -0
  16. data/spec/oauth2/client_spec.rb +506 -0
  17. data/spec/oauth2/mac_token_spec.rb +117 -0
  18. data/spec/oauth2/response_spec.rb +90 -0
  19. data/spec/oauth2/strategy/assertion_spec.rb +58 -0
  20. data/spec/oauth2/strategy/auth_code_spec.rb +107 -0
  21. data/spec/oauth2/strategy/base_spec.rb +5 -0
  22. data/spec/oauth2/strategy/client_credentials_spec.rb +69 -0
  23. data/spec/oauth2/strategy/implicit_spec.rb +26 -0
  24. data/spec/oauth2/strategy/password_spec.rb +55 -0
  25. data/spec/oauth2/version_spec.rb +23 -0
  26. metadata +41 -38
  27. data/.document +0 -5
  28. data/.gitignore +0 -19
  29. data/.jrubyrc +0 -1
  30. data/.rspec +0 -2
  31. data/.rubocop.yml +0 -80
  32. data/.rubocop_rspec.yml +0 -26
  33. data/.rubocop_todo.yml +0 -15
  34. data/.ruby-version +0 -1
  35. data/.travis.yml +0 -70
  36. data/CONTRIBUTING.md +0 -18
  37. data/Gemfile +0 -40
  38. data/Rakefile +0 -45
  39. data/gemfiles/jruby_1.7.gemfile +0 -11
  40. data/gemfiles/jruby_9.0.gemfile +0 -7
  41. data/gemfiles/jruby_9.1.gemfile +0 -3
  42. data/gemfiles/jruby_9.2.gemfile +0 -3
  43. data/gemfiles/jruby_head.gemfile +0 -3
  44. data/gemfiles/ruby_1.9.gemfile +0 -11
  45. data/gemfiles/ruby_2.0.gemfile +0 -6
  46. data/gemfiles/ruby_2.1.gemfile +0 -6
  47. data/gemfiles/ruby_2.2.gemfile +0 -3
  48. data/gemfiles/ruby_2.3.gemfile +0 -3
  49. data/gemfiles/ruby_2.4.gemfile +0 -3
  50. data/gemfiles/ruby_2.5.gemfile +0 -3
  51. data/gemfiles/ruby_2.6.gemfile +0 -9
  52. data/gemfiles/ruby_head.gemfile +0 -9
  53. data/gemfiles/truffleruby.gemfile +0 -3
  54. data/oauth2.gemspec +0 -44
@@ -98,9 +98,17 @@ module OAuth2
98
98
  @algorithm = begin
99
99
  case alg.to_s
100
100
  when 'hmac-sha-1'
101
- OpenSSL::Digest::SHA1.new
101
+ begin
102
+ OpenSSL::Digest('SHA1').new
103
+ rescue StandardError
104
+ OpenSSL::Digest.new('SHA1')
105
+ end
102
106
  when 'hmac-sha-256'
103
- OpenSSL::Digest::SHA256.new
107
+ begin
108
+ OpenSSL::Digest('SHA256').new
109
+ rescue StandardError
110
+ OpenSSL::Digest.new('SHA256')
111
+ end
104
112
  else
105
113
  raise(ArgumentError, 'Unsupported algorithm')
106
114
  end
@@ -11,9 +11,9 @@ module OAuth2
11
11
  # Procs that, when called, will parse a response body according
12
12
  # to the specified format.
13
13
  @@parsers = {
14
- :json => lambda { |body| MultiJson.load(body) rescue body }, # rubocop:disable RescueModifier
14
+ :json => lambda { |body| MultiJson.load(body) rescue body }, # rubocop:disable Style/RescueModifier
15
15
  :query => lambda { |body| Rack::Utils.parse_query(body) },
16
- :text => lambda { |body| body },
16
+ :text => lambda { |body| body },
17
17
  }
18
18
 
19
19
  # Content type assignments for various potential HTTP content types.
@@ -68,6 +68,7 @@ module OAuth2
68
68
  # application/json Content-Type response bodies
69
69
  def parsed
70
70
  return nil unless @@parsers.key?(parser)
71
+
71
72
  @parsed ||= @@parsers[parser].call(body)
72
73
  end
73
74
 
@@ -79,11 +80,12 @@ module OAuth2
79
80
  # Determines the parser that will be used to supply the content of #parsed
80
81
  def parser
81
82
  return options[:parse].to_sym if @@parsers.key?(options[:parse])
83
+
82
84
  @@content_types[content_type]
83
85
  end
84
86
  end
85
87
  end
86
88
 
87
89
  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
90
+ MultiXml.parse(body) rescue body # rubocop:disable Style/RescueModifier
89
91
  end
@@ -50,10 +50,10 @@ module OAuth2
50
50
  def build_request(params)
51
51
  assertion = build_assertion(params)
52
52
  {
53
- :grant_type => 'assertion',
53
+ :grant_type => 'assertion',
54
54
  :assertion_type => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
55
- :assertion => assertion,
56
- :scope => params[:scope],
55
+ :assertion => assertion,
56
+ :scope => params[:scope],
57
57
  }
58
58
  end
59
59
 
@@ -18,8 +18,8 @@ module OAuth2
18
18
  # @param [Hash] params additional params
19
19
  def get_token(username, password, params = {}, opts = {})
20
20
  params = {'grant_type' => 'password',
21
- 'username' => username,
22
- 'password' => password}.merge(params)
21
+ 'username' => username,
22
+ 'password' => password}.merge(params)
23
23
  @client.get_token(params, opts)
24
24
  end
25
25
  end
@@ -1,5 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module OAuth2
2
4
  module Version
5
+ VERSION = to_s
6
+
3
7
  module_function
4
8
 
5
9
  # The major version
@@ -20,12 +24,12 @@ module OAuth2
20
24
  #
21
25
  # @return [Integer]
22
26
  def patch
23
- 2
27
+ 7
24
28
  end
25
29
 
26
30
  # The pre-release version, if any
27
31
  #
28
- # @return [Integer, NilClass]
32
+ # @return [String, NilClass]
29
33
  def pre
30
34
  nil
31
35
  end
@@ -53,7 +57,9 @@ module OAuth2
53
57
  #
54
58
  # @return [String]
55
59
  def to_s
56
- to_a.join('.')
60
+ v = [major, minor, patch].compact.join('.')
61
+ v += "-#{pre}" if pre
62
+ v
57
63
  end
58
64
  end
59
65
  end
data/spec/helper.rb ADDED
@@ -0,0 +1,37 @@
1
+ DEBUG = ENV['DEBUG'] == 'true'
2
+
3
+ ruby_version = Gem::Version.new(RUBY_VERSION)
4
+
5
+ if ruby_version >= Gem::Version.new('2.7')
6
+ require 'simplecov'
7
+ require 'coveralls'
8
+
9
+ SimpleCov.formatters = [SimpleCov::Formatter::HTMLFormatter, Coveralls::SimpleCov::Formatter]
10
+
11
+ SimpleCov.start do
12
+ add_filter '/spec'
13
+ minimum_coverage(95)
14
+ end
15
+ end
16
+
17
+ require 'byebug' if DEBUG && ruby_version >= Gem::Version.new('2.4')
18
+
19
+ require 'oauth2'
20
+ require 'addressable/uri'
21
+ require 'rspec'
22
+ require 'rspec/stubbed_env'
23
+ require 'silent_stream'
24
+
25
+ RSpec.configure do |config|
26
+ config.expect_with :rspec do |c|
27
+ c.syntax = :expect
28
+ end
29
+ end
30
+
31
+ Faraday.default_adapter = :test
32
+
33
+ RSpec.configure do |conf|
34
+ conf.include SilentStream
35
+ end
36
+
37
+ VERBS = [:get, :post, :put, :delete].freeze
@@ -0,0 +1,216 @@
1
+ describe OAuth2::AccessToken do
2
+ subject { described_class.new(client, token) }
3
+
4
+ let(:token) { 'monkey' }
5
+ let(:refresh_body) { MultiJson.encode(:access_token => 'refreshed_foo', :expires_in => 600, :refresh_token => 'refresh_bar') }
6
+ let(:client) do
7
+ OAuth2::Client.new('abc', 'def', :site => 'https://api.example.com') do |builder|
8
+ builder.request :url_encoded
9
+ builder.adapter :test do |stub|
10
+ VERBS.each do |verb|
11
+ stub.send(verb, '/token/header') { |env| [200, {}, env[:request_headers]['Authorization']] }
12
+ stub.send(verb, "/token/query?access_token=#{token}") { |env| [200, {}, Addressable::URI.parse(env[:url]).query_values['access_token']] }
13
+ stub.send(verb, '/token/query_string') { |env| [200, {}, CGI.unescape(Addressable::URI.parse(env[:url]).query)] }
14
+ stub.send(verb, '/token/body') { |env| [200, {}, env[:body]] }
15
+ end
16
+ stub.post('/oauth/token') { |env| [200, {'Content-Type' => 'application/json'}, refresh_body] }
17
+ end
18
+ end
19
+ end
20
+
21
+ describe '#initialize' do
22
+ it 'assigns client and token' do
23
+ expect(subject.client).to eq(client)
24
+ expect(subject.token).to eq(token)
25
+ end
26
+
27
+ it 'assigns extra params' do
28
+ target = described_class.new(client, token, 'foo' => 'bar')
29
+ expect(target.params).to include('foo')
30
+ expect(target.params['foo']).to eq('bar')
31
+ end
32
+
33
+ def assert_initialized_token(target) # rubocop:disable Metrics/AbcSize
34
+ expect(target.token).to eq(token)
35
+ expect(target).to be_expires
36
+ expect(target.params.keys).to include('foo')
37
+ expect(target.params['foo']).to eq('bar')
38
+ end
39
+
40
+ it 'initializes with a Hash' do
41
+ hash = {:access_token => token, :expires_at => Time.now.to_i + 200, 'foo' => 'bar'}
42
+ target = described_class.from_hash(client, hash)
43
+ assert_initialized_token(target)
44
+ end
45
+
46
+ it 'from_hash does not modify opts hash' do
47
+ hash = {:access_token => token, :expires_at => Time.now.to_i}
48
+ hash_before = hash.dup
49
+ described_class.from_hash(client, hash)
50
+ expect(hash).to eq(hash_before)
51
+ end
52
+
53
+ it 'initializes with a form-urlencoded key/value string' do
54
+ kvform = "access_token=#{token}&expires_at=#{Time.now.to_i + 200}&foo=bar"
55
+ target = described_class.from_kvform(client, kvform)
56
+ assert_initialized_token(target)
57
+ end
58
+
59
+ it 'sets options' do
60
+ target = described_class.new(client, token, :param_name => 'foo', :header_format => 'Bearer %', :mode => :body)
61
+ expect(target.options[:param_name]).to eq('foo')
62
+ expect(target.options[:header_format]).to eq('Bearer %')
63
+ expect(target.options[:mode]).to eq(:body)
64
+ end
65
+
66
+ it 'does not modify opts hash' do
67
+ opts = {:param_name => 'foo', :header_format => 'Bearer %', :mode => :body}
68
+ opts_before = opts.dup
69
+ described_class.new(client, token, opts)
70
+ expect(opts).to eq(opts_before)
71
+ end
72
+
73
+ describe 'expires_at' do
74
+ let(:expires_at) { 1_361_396_829 }
75
+ let(:hash) do
76
+ {
77
+ :access_token => token,
78
+ :expires_at => expires_at.to_s,
79
+ 'foo' => 'bar',
80
+ }
81
+ end
82
+
83
+ it 'initializes with an integer timestamp expires_at' do
84
+ target = described_class.from_hash(client, hash.merge(:expires_at => expires_at))
85
+ assert_initialized_token(target)
86
+ expect(target.expires_at).to eql(expires_at)
87
+ end
88
+
89
+ it 'initializes with a string timestamp expires_at' do
90
+ target = described_class.from_hash(client, hash)
91
+ assert_initialized_token(target)
92
+ expect(target.expires_at).to eql(expires_at)
93
+ end
94
+
95
+ it 'initializes with a string time expires_at' do
96
+ target = described_class.from_hash(client, hash.merge(:expires_at => Time.at(expires_at).iso8601))
97
+ assert_initialized_token(target)
98
+ expect(target.expires_at).to eql(expires_at)
99
+ end
100
+ end
101
+ end
102
+
103
+ describe '#request' do
104
+ context 'with :mode => :header' do
105
+ before do
106
+ subject.options[:mode] = :header
107
+ end
108
+
109
+ VERBS.each do |verb|
110
+ it "sends the token in the Authorization header for a #{verb.to_s.upcase} request" do
111
+ expect(subject.post('/token/header').body).to include(token)
112
+ end
113
+ end
114
+ end
115
+
116
+ context 'with :mode => :query' do
117
+ before do
118
+ subject.options[:mode] = :query
119
+ end
120
+
121
+ VERBS.each do |verb|
122
+ it "sends the token in the Authorization header for a #{verb.to_s.upcase} request" do
123
+ expect(subject.post('/token/query').body).to eq(token)
124
+ end
125
+
126
+ it "sends a #{verb.to_s.upcase} request and options[:param_name] include [number]." do
127
+ subject.options[:param_name] = 'auth[1]'
128
+ expect(subject.__send__(verb, '/token/query_string').body).to include("auth[1]=#{token}")
129
+ end
130
+ end
131
+ end
132
+
133
+ context 'with :mode => :body' do
134
+ before do
135
+ subject.options[:mode] = :body
136
+ end
137
+
138
+ VERBS.each do |verb|
139
+ it "sends the token in the Authorization header for a #{verb.to_s.upcase} request" do
140
+ expect(subject.post('/token/body').body.split('=').last).to eq(token)
141
+ end
142
+ end
143
+ end
144
+
145
+ context 'params include [number]' do
146
+ VERBS.each do |verb|
147
+ it "sends #{verb.to_s.upcase} correct query" do
148
+ expect(subject.__send__(verb, '/token/query_string', :params => {'foo[bar][1]' => 'val'}).body).to include('foo[bar][1]=val')
149
+ end
150
+ end
151
+ end
152
+ end
153
+
154
+ describe '#expires?' do
155
+ it 'is false if there is no expires_at' do
156
+ expect(described_class.new(client, token)).not_to be_expires
157
+ end
158
+
159
+ it 'is true if there is an expires_in' do
160
+ expect(described_class.new(client, token, :refresh_token => 'abaca', :expires_in => 600)).to be_expires
161
+ end
162
+
163
+ it 'is true if there is an expires_at' do
164
+ expect(described_class.new(client, token, :refresh_token => 'abaca', :expires_in => Time.now.getutc.to_i + 600)).to be_expires
165
+ end
166
+ end
167
+
168
+ describe '#expired?' do
169
+ it 'is false if there is no expires_in or expires_at' do
170
+ expect(described_class.new(client, token)).not_to be_expired
171
+ end
172
+
173
+ it 'is false if expires_in is in the future' do
174
+ expect(described_class.new(client, token, :refresh_token => 'abaca', :expires_in => 10_800)).not_to be_expired
175
+ end
176
+
177
+ it 'is true if expires_at is in the past' do
178
+ access = described_class.new(client, token, :refresh_token => 'abaca', :expires_in => 600)
179
+ @now = Time.now + 10_800
180
+ allow(Time).to receive(:now).and_return(@now)
181
+ expect(access).to be_expired
182
+ end
183
+ end
184
+
185
+ describe '#refresh!' do
186
+ let(:access) do
187
+ described_class.new(client, token, :refresh_token => 'abaca',
188
+ :expires_in => 600,
189
+ :param_name => 'o_param')
190
+ end
191
+
192
+ it 'returns a refresh token with appropriate values carried over' do
193
+ refreshed = access.refresh!
194
+ expect(access.client).to eq(refreshed.client)
195
+ expect(access.options[:param_name]).to eq(refreshed.options[:param_name])
196
+ end
197
+
198
+ context 'with a nil refresh_token in the response' do
199
+ let(:refresh_body) { MultiJson.encode(:access_token => 'refreshed_foo', :expires_in => 600, :refresh_token => nil) }
200
+
201
+ it 'copies the refresh_token from the original token' do
202
+ refreshed = access.refresh!
203
+
204
+ expect(refreshed.refresh_token).to eq(access.refresh_token)
205
+ end
206
+ end
207
+ end
208
+
209
+ describe '#to_hash' do
210
+ it 'return a hash equals to the hash used to initialize access token' do
211
+ hash = {:access_token => token, :refresh_token => 'foobar', :expires_at => Time.now.to_i + 200, 'foo' => 'bar'}
212
+ access_token = described_class.from_hash(client, hash.clone)
213
+ expect(access_token.to_hash).to eq(hash)
214
+ end
215
+ end
216
+ end
@@ -0,0 +1,84 @@
1
+ describe OAuth2::Authenticator do
2
+ subject do
3
+ described_class.new(client_id, client_secret, mode)
4
+ end
5
+
6
+ let(:client_id) { 'foo' }
7
+ let(:client_secret) { 'bar' }
8
+ let(:mode) { :undefined }
9
+
10
+ it 'raises NotImplementedError for unknown authentication mode' do
11
+ expect { subject.apply({}) }.to raise_error(NotImplementedError)
12
+ end
13
+
14
+ describe '#apply' do
15
+ context 'with parameter-based authentication' do
16
+ let(:mode) { :request_body }
17
+
18
+ it 'adds client_id and client_secret to params' do
19
+ output = subject.apply({})
20
+ expect(output).to eq('client_id' => 'foo', 'client_secret' => 'bar')
21
+ end
22
+
23
+ it 'does not overwrite existing credentials' do
24
+ input = {'client_secret' => 's3cr3t'}
25
+ output = subject.apply(input)
26
+ expect(output).to eq('client_id' => 'foo', 'client_secret' => 's3cr3t')
27
+ end
28
+
29
+ it 'preserves other parameters' do
30
+ input = {'state' => '42', :headers => {'A' => 'b'}}
31
+ output = subject.apply(input)
32
+ expect(output).to eq(
33
+ 'client_id' => 'foo',
34
+ 'client_secret' => 'bar',
35
+ 'state' => '42',
36
+ :headers => {'A' => 'b'}
37
+ )
38
+ end
39
+
40
+ context 'using tls client authentication' do
41
+ let(:mode) { :tls_client_auth }
42
+
43
+ it 'does not add client_secret' do
44
+ output = subject.apply({})
45
+ expect(output).to eq('client_id' => 'foo')
46
+ end
47
+ end
48
+
49
+ context 'using private key jwt authentication' do
50
+ let(:mode) { :private_key_jwt }
51
+
52
+ it 'does not add client_secret or client_id' do
53
+ output = subject.apply({})
54
+ expect(output).to eq({})
55
+ end
56
+ end
57
+ end
58
+
59
+ context 'with Basic authentication' do
60
+ let(:mode) { :basic_auth }
61
+ let(:header) { 'Basic ' + Base64.encode64("#{client_id}:#{client_secret}").delete("\n") }
62
+
63
+ it 'encodes credentials in headers' do
64
+ output = subject.apply({})
65
+ expect(output).to eq(:headers => {'Authorization' => header})
66
+ end
67
+
68
+ it 'does not overwrite existing credentials' do
69
+ input = {:headers => {'Authorization' => 'Bearer abc123'}}
70
+ output = subject.apply(input)
71
+ expect(output).to eq(:headers => {'Authorization' => 'Bearer abc123'})
72
+ end
73
+
74
+ it 'does not overwrite existing params or headers' do
75
+ input = {'state' => '42', :headers => {'A' => 'b'}}
76
+ output = subject.apply(input)
77
+ expect(output).to eq(
78
+ 'state' => '42',
79
+ :headers => {'A' => 'b', 'Authorization' => header}
80
+ )
81
+ end
82
+ end
83
+ end
84
+ end