oauth2 1.4.7 → 2.0.20

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 (58) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +811 -76
  4. data/CITATION.cff +20 -0
  5. data/CODE_OF_CONDUCT.md +24 -23
  6. data/CONTRIBUTING.md +221 -0
  7. data/FUNDING.md +74 -0
  8. data/IRP.md +107 -0
  9. data/{LICENSE → LICENSE.txt} +2 -2
  10. data/OIDC.md +167 -0
  11. data/README.md +1468 -166
  12. data/REEK +2 -0
  13. data/RUBOCOP.md +71 -0
  14. data/SECURITY.md +24 -0
  15. data/THREAT_MODEL.md +94 -0
  16. data/lib/oauth2/access_token.rb +276 -40
  17. data/lib/oauth2/auth_sanitizer.rb +36 -0
  18. data/lib/oauth2/authenticator.rb +51 -10
  19. data/lib/oauth2/client.rb +444 -124
  20. data/lib/oauth2/error.rb +63 -24
  21. data/lib/oauth2/filtered_attributes.rb +10 -0
  22. data/lib/oauth2/response.rb +138 -43
  23. data/lib/oauth2/strategy/assertion.rb +71 -41
  24. data/lib/oauth2/strategy/auth_code.rb +28 -5
  25. data/lib/oauth2/strategy/base.rb +2 -0
  26. data/lib/oauth2/strategy/client_credentials.rb +6 -4
  27. data/lib/oauth2/strategy/implicit.rb +20 -3
  28. data/lib/oauth2/strategy/password.rb +17 -5
  29. data/lib/oauth2/version.rb +2 -59
  30. data/lib/oauth2.rb +103 -12
  31. data/sig/oauth2/access_token.rbs +25 -0
  32. data/sig/oauth2/authenticator.rbs +22 -0
  33. data/sig/oauth2/client.rbs +52 -0
  34. data/sig/oauth2/error.rbs +8 -0
  35. data/sig/oauth2/filtered_attributes.rbs +11 -0
  36. data/sig/oauth2/response.rbs +18 -0
  37. data/sig/oauth2/sanitized_logger.rbs +32 -0
  38. data/sig/oauth2/strategy.rbs +34 -0
  39. data/sig/oauth2/thing_filter.rbs +10 -0
  40. data/sig/oauth2/version.rbs +5 -0
  41. data/sig/oauth2.rbs +9 -0
  42. data.tar.gz.sig +0 -0
  43. metadata +293 -102
  44. metadata.gz.sig +4 -0
  45. data/lib/oauth2/mac_token.rb +0 -130
  46. data/spec/helper.rb +0 -37
  47. data/spec/oauth2/access_token_spec.rb +0 -216
  48. data/spec/oauth2/authenticator_spec.rb +0 -84
  49. data/spec/oauth2/client_spec.rb +0 -506
  50. data/spec/oauth2/mac_token_spec.rb +0 -117
  51. data/spec/oauth2/response_spec.rb +0 -90
  52. data/spec/oauth2/strategy/assertion_spec.rb +0 -58
  53. data/spec/oauth2/strategy/auth_code_spec.rb +0 -107
  54. data/spec/oauth2/strategy/base_spec.rb +0 -5
  55. data/spec/oauth2/strategy/client_credentials_spec.rb +0 -69
  56. data/spec/oauth2/strategy/implicit_spec.rb +0 -26
  57. data/spec/oauth2/strategy/password_spec.rb +0 -55
  58. data/spec/oauth2/version_spec.rb +0 -23
@@ -1,130 +0,0 @@
1
- require 'base64'
2
- require 'digest'
3
- require 'openssl'
4
- require 'securerandom'
5
-
6
- module OAuth2
7
- class MACToken < AccessToken
8
- # Generates a MACToken from an AccessToken and secret
9
- #
10
- # @param [AccessToken] token the OAuth2::Token instance
11
- # @option [String] secret the secret key value
12
- # @param [Hash] opts the options to create the Access Token with
13
- # @see MACToken#initialize
14
- def self.from_access_token(token, secret, options = {})
15
- new(token.client, token.token, secret, token.params.merge(:refresh_token => token.refresh_token, :expires_in => token.expires_in, :expires_at => token.expires_at).merge(options))
16
- end
17
-
18
- attr_reader :secret, :algorithm
19
-
20
- # Initalize a MACToken
21
- #
22
- # @param [Client] client the OAuth2::Client instance
23
- # @param [String] token the Access Token value
24
- # @option [String] secret the secret key value
25
- # @param [Hash] opts the options to create the Access Token with
26
- # @option opts [String] :refresh_token (nil) the refresh_token value
27
- # @option opts [FixNum, String] :expires_in (nil) the number of seconds in which the AccessToken will expire
28
- # @option opts [FixNum, String] :expires_at (nil) the epoch time in seconds in which AccessToken will expire
29
- # @option opts [FixNum, String] :algorithm (hmac-sha-256) the algorithm to use for the HMAC digest (one of 'hmac-sha-256', 'hmac-sha-1')
30
- def initialize(client, token, secret, opts = {})
31
- @secret = secret
32
- self.algorithm = opts.delete(:algorithm) || 'hmac-sha-256'
33
-
34
- super(client, token, opts)
35
- end
36
-
37
- # Make a request with the MAC Token
38
- #
39
- # @param [Symbol] verb the HTTP request method
40
- # @param [String] path the HTTP URL path of the request
41
- # @param [Hash] opts the options to make the request with
42
- # @see Client#request
43
- def request(verb, path, opts = {}, &block)
44
- url = client.connection.build_url(path, opts[:params]).to_s
45
-
46
- opts[:headers] ||= {}
47
- opts[:headers]['Authorization'] = header(verb, url)
48
-
49
- @client.request(verb, path, opts, &block)
50
- end
51
-
52
- # Get the headers hash (always an empty hash)
53
- def headers
54
- {}
55
- end
56
-
57
- # Generate the MAC header
58
- #
59
- # @param [Symbol] verb the HTTP request method
60
- # @param [String] url the HTTP URL path of the request
61
- def header(verb, url)
62
- timestamp = Time.now.utc.to_i
63
- nonce = Digest::MD5.hexdigest([timestamp, SecureRandom.hex].join(':'))
64
-
65
- uri = URI.parse(url)
66
-
67
- raise(ArgumentError, "could not parse \"#{url}\" into URI") unless uri.is_a?(URI::HTTP)
68
-
69
- mac = signature(timestamp, nonce, verb, uri)
70
-
71
- "MAC id=\"#{token}\", ts=\"#{timestamp}\", nonce=\"#{nonce}\", mac=\"#{mac}\""
72
- end
73
-
74
- # Generate the Base64-encoded HMAC digest signature
75
- #
76
- # @param [Fixnum] timestamp the timestamp of the request in seconds since epoch
77
- # @param [String] nonce the MAC header nonce
78
- # @param [Symbol] verb the HTTP request method
79
- # @param [String] url the HTTP URL path of the request
80
- def signature(timestamp, nonce, verb, uri)
81
- signature = [
82
- timestamp,
83
- nonce,
84
- verb.to_s.upcase,
85
- uri.request_uri,
86
- uri.host,
87
- uri.port,
88
- '', nil
89
- ].join("\n")
90
-
91
- strict_encode64(OpenSSL::HMAC.digest(@algorithm, secret, signature))
92
- end
93
-
94
- # Set the HMAC algorithm
95
- #
96
- # @param [String] alg the algorithm to use (one of 'hmac-sha-1', 'hmac-sha-256')
97
- def algorithm=(alg)
98
- @algorithm = begin
99
- case alg.to_s
100
- when 'hmac-sha-1'
101
- begin
102
- OpenSSL::Digest('SHA1').new
103
- rescue StandardError
104
- OpenSSL::Digest.new('SHA1')
105
- end
106
- when 'hmac-sha-256'
107
- begin
108
- OpenSSL::Digest('SHA256').new
109
- rescue StandardError
110
- OpenSSL::Digest.new('SHA256')
111
- end
112
- else
113
- raise(ArgumentError, 'Unsupported algorithm')
114
- end
115
- end
116
- end
117
-
118
- private
119
-
120
- # No-op since we need the verb and path
121
- # and the MAC always goes in a header
122
- def token=(_noop)
123
- end
124
-
125
- # Base64.strict_encode64 is not available on Ruby 1.8.7
126
- def strict_encode64(str)
127
- Base64.encode64(str).delete("\n")
128
- end
129
- end
130
- end
data/spec/helper.rb DELETED
@@ -1,37 +0,0 @@
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
@@ -1,216 +0,0 @@
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
@@ -1,84 +0,0 @@
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