oauth2 1.4.4 → 1.4.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -2
  3. data/CODE_OF_CONDUCT.md +105 -46
  4. data/README.md +8 -3
  5. data/lib/oauth2/access_token.rb +8 -7
  6. data/lib/oauth2/authenticator.rb +1 -1
  7. data/lib/oauth2/client.rb +60 -14
  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 +38 -41
  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 -87
  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_2.7.gemfile +0 -9
  53. data/gemfiles/ruby_head.gemfile +0 -9
  54. data/gemfiles/truffleruby.gemfile +0 -3
  55. data/oauth2.gemspec +0 -52
@@ -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
- 4
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