oauth2 1.4.4 → 1.4.8
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +32 -2
- data/CODE_OF_CONDUCT.md +105 -46
- data/LICENSE +1 -1
- data/README.md +277 -112
- data/lib/oauth2/access_token.rb +8 -7
- data/lib/oauth2/authenticator.rb +1 -1
- data/lib/oauth2/client.rb +64 -21
- data/lib/oauth2/error.rb +1 -1
- data/lib/oauth2/mac_token.rb +16 -10
- data/lib/oauth2/response.rb +5 -3
- data/lib/oauth2/strategy/assertion.rb +3 -3
- data/lib/oauth2/strategy/password.rb +2 -2
- data/lib/oauth2/version.rb +9 -3
- data/spec/helper.rb +30 -0
- data/spec/oauth2/access_token_spec.rb +216 -0
- data/spec/oauth2/authenticator_spec.rb +84 -0
- data/spec/oauth2/client_spec.rb +530 -0
- data/spec/oauth2/mac_token_spec.rb +120 -0
- data/spec/oauth2/response_spec.rb +90 -0
- data/spec/oauth2/strategy/assertion_spec.rb +59 -0
- data/spec/oauth2/strategy/auth_code_spec.rb +107 -0
- data/spec/oauth2/strategy/base_spec.rb +5 -0
- data/spec/oauth2/strategy/client_credentials_spec.rb +69 -0
- data/spec/oauth2/strategy/implicit_spec.rb +26 -0
- data/spec/oauth2/strategy/password_spec.rb +56 -0
- data/spec/oauth2/version_spec.rb +23 -0
- metadata +41 -57
- data/.document +0 -5
- data/.gitignore +0 -19
- data/.jrubyrc +0 -1
- data/.rspec +0 -2
- data/.rubocop.yml +0 -80
- data/.rubocop_rspec.yml +0 -26
- data/.rubocop_todo.yml +0 -15
- data/.ruby-version +0 -1
- data/.travis.yml +0 -87
- data/CONTRIBUTING.md +0 -18
- data/Gemfile +0 -40
- data/Rakefile +0 -45
- data/gemfiles/jruby_1.7.gemfile +0 -11
- data/gemfiles/jruby_9.0.gemfile +0 -7
- data/gemfiles/jruby_9.1.gemfile +0 -3
- data/gemfiles/jruby_9.2.gemfile +0 -3
- data/gemfiles/jruby_head.gemfile +0 -3
- data/gemfiles/ruby_1.9.gemfile +0 -11
- data/gemfiles/ruby_2.0.gemfile +0 -6
- data/gemfiles/ruby_2.1.gemfile +0 -6
- data/gemfiles/ruby_2.2.gemfile +0 -3
- data/gemfiles/ruby_2.3.gemfile +0 -3
- data/gemfiles/ruby_2.4.gemfile +0 -3
- data/gemfiles/ruby_2.5.gemfile +0 -3
- data/gemfiles/ruby_2.6.gemfile +0 -9
- data/gemfiles/ruby_2.7.gemfile +0 -9
- data/gemfiles/ruby_head.gemfile +0 -9
- data/gemfiles/truffleruby.gemfile +0 -3
- data/oauth2.gemspec +0 -52
data/lib/oauth2/client.rb
CHANGED
@@ -4,6 +4,8 @@ require 'logger'
|
|
4
4
|
module OAuth2
|
5
5
|
# The OAuth2::Client class
|
6
6
|
class Client # rubocop:disable Metrics/ClassLength
|
7
|
+
RESERVED_PARAM_KEYS = %w[headers parse].freeze
|
8
|
+
|
7
9
|
attr_reader :id, :secret, :site
|
8
10
|
attr_accessor :options
|
9
11
|
attr_writer :connection
|
@@ -23,8 +25,8 @@ module OAuth2
|
|
23
25
|
# @option opts [Symbol] :auth_scheme (:basic_auth) HTTP method to use to authorize request (:basic_auth or :request_body)
|
24
26
|
# @option opts [Hash] :connection_opts ({}) Hash of connection options to pass to initialize Faraday with
|
25
27
|
# @option opts [FixNum] :max_redirects (5) maximum number of redirects to follow
|
26
|
-
# @option opts [Boolean] :raise_errors (true) whether or not to raise an OAuth2::Error
|
27
|
-
#
|
28
|
+
# @option opts [Boolean] :raise_errors (true) whether or not to raise an OAuth2::Error on responses with 400+ status codes
|
29
|
+
# @option opts [Proc] :extract_access_token proc that extracts the access token from the response
|
28
30
|
# @yield [builder] The Faraday connection builder
|
29
31
|
def initialize(client_id, client_secret, options = {}, &block)
|
30
32
|
opts = options.dup
|
@@ -32,14 +34,18 @@ module OAuth2
|
|
32
34
|
@secret = client_secret
|
33
35
|
@site = opts.delete(:site)
|
34
36
|
ssl = opts.delete(:ssl)
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
37
|
+
|
38
|
+
@options = {
|
39
|
+
:authorize_url => '/oauth/authorize',
|
40
|
+
:token_url => '/oauth/token',
|
41
|
+
:token_method => :post,
|
42
|
+
:auth_scheme => :request_body,
|
43
|
+
:connection_opts => {},
|
44
|
+
:connection_build => block,
|
45
|
+
:max_redirects => 5,
|
46
|
+
:raise_errors => true,
|
47
|
+
:extract_access_token => DEFAULT_EXTRACT_ACCESS_TOKEN,
|
48
|
+
}.merge(opts)
|
43
49
|
@options[:connection_opts][:ssl] = ssl if ssl
|
44
50
|
end
|
45
51
|
|
@@ -53,15 +59,12 @@ module OAuth2
|
|
53
59
|
|
54
60
|
# The Faraday connection object
|
55
61
|
def connection
|
56
|
-
@connection ||=
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
options[:connection_build].call(b)
|
62
|
+
@connection ||=
|
63
|
+
Faraday.new(site, options[:connection_opts]) do |builder|
|
64
|
+
if options[:connection_build]
|
65
|
+
options[:connection_build].call(builder)
|
61
66
|
end
|
62
67
|
end
|
63
|
-
conn
|
64
|
-
end
|
65
68
|
end
|
66
69
|
|
67
70
|
# The authorize endpoint URL of the OAuth2 provider
|
@@ -91,7 +94,7 @@ module OAuth2
|
|
91
94
|
# code response for this request. Will default to client option
|
92
95
|
# @option opts [Symbol] :parse @see Response::initialize
|
93
96
|
# @yield [req] The Faraday request
|
94
|
-
def request(verb, url, opts = {}) # rubocop:disable
|
97
|
+
def request(verb, url, opts = {}) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
95
98
|
connection.response :logger, ::Logger.new($stdout) if ENV['OAUTH_DEBUG'] == 'true'
|
96
99
|
|
97
100
|
url = connection.build_url(url).to_s
|
@@ -107,6 +110,7 @@ module OAuth2
|
|
107
110
|
opts[:redirect_count] ||= 0
|
108
111
|
opts[:redirect_count] += 1
|
109
112
|
return response if opts[:redirect_count] > options[:max_redirects]
|
113
|
+
|
110
114
|
if response.status == 303
|
111
115
|
verb = :get
|
112
116
|
opts.delete(:body)
|
@@ -118,6 +122,7 @@ module OAuth2
|
|
118
122
|
when 400..599
|
119
123
|
error = Error.new(response)
|
120
124
|
raise(error) if opts.fetch(:raise_errors, options[:raise_errors])
|
125
|
+
|
121
126
|
response.error = error
|
122
127
|
response
|
123
128
|
else
|
@@ -132,7 +137,16 @@ module OAuth2
|
|
132
137
|
# @param [Hash] access token options, to pass to the AccessToken object
|
133
138
|
# @param [Class] class of access token for easier subclassing OAuth2::AccessToken
|
134
139
|
# @return [AccessToken] the initialized AccessToken
|
135
|
-
def get_token(params, access_token_opts = {},
|
140
|
+
def get_token(params, access_token_opts = {}, extract_access_token = options[:extract_access_token]) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
141
|
+
params = params.map do |key, value|
|
142
|
+
if RESERVED_PARAM_KEYS.include?(key)
|
143
|
+
[key.to_sym, value]
|
144
|
+
else
|
145
|
+
[key, value]
|
146
|
+
end
|
147
|
+
end
|
148
|
+
params = Hash[params]
|
149
|
+
|
136
150
|
params = Authenticator.new(id, secret, options[:auth_scheme]).apply(params)
|
137
151
|
opts = {:raise_errors => options[:raise_errors], :parse => params.delete(:parse)}
|
138
152
|
headers = params.delete(:headers) || {}
|
@@ -145,11 +159,18 @@ module OAuth2
|
|
145
159
|
end
|
146
160
|
opts[:headers].merge!(headers)
|
147
161
|
response = request(options[:token_method], token_url, opts)
|
148
|
-
|
162
|
+
|
163
|
+
access_token = begin
|
164
|
+
build_access_token(response, access_token_opts, extract_access_token)
|
165
|
+
rescue StandardError
|
166
|
+
nil
|
167
|
+
end
|
168
|
+
|
169
|
+
if options[:raise_errors] && !access_token
|
149
170
|
error = Error.new(response)
|
150
171
|
raise(error)
|
151
172
|
end
|
152
|
-
|
173
|
+
access_token
|
153
174
|
end
|
154
175
|
|
155
176
|
# The Authorization Code strategy
|
@@ -207,5 +228,27 @@ module OAuth2
|
|
207
228
|
{}
|
208
229
|
end
|
209
230
|
end
|
231
|
+
|
232
|
+
DEFAULT_EXTRACT_ACCESS_TOKEN = proc do |client, hash|
|
233
|
+
token = hash.delete('access_token') || hash.delete(:access_token)
|
234
|
+
token && AccessToken.new(client, token, hash)
|
235
|
+
end
|
236
|
+
|
237
|
+
private
|
238
|
+
|
239
|
+
def build_access_token(response, access_token_opts, extract_access_token)
|
240
|
+
parsed_response = response.parsed.dup
|
241
|
+
return unless parsed_response.is_a?(Hash)
|
242
|
+
|
243
|
+
hash = parsed_response.merge(access_token_opts)
|
244
|
+
|
245
|
+
# Provide backwards compatibility for old AcessToken.form_hash pattern
|
246
|
+
# Should be deprecated in 2.x
|
247
|
+
if extract_access_token.is_a?(Class) && extract_access_token.respond_to?(:from_hash)
|
248
|
+
extract_access_token.from_hash(self, hash)
|
249
|
+
else
|
250
|
+
extract_access_token.call(self, hash)
|
251
|
+
end
|
252
|
+
end
|
210
253
|
end
|
211
254
|
end
|
data/lib/oauth2/error.rb
CHANGED
@@ -23,7 +23,7 @@ module OAuth2
|
|
23
23
|
def error_message(response_body, opts = {})
|
24
24
|
message = []
|
25
25
|
|
26
|
-
opts[:error_description] && message << opts[:error_description]
|
26
|
+
opts[:error_description] && (message << opts[:error_description])
|
27
27
|
|
28
28
|
error_message = if opts[:error_description] && opts[:error_description].respond_to?(:encoding)
|
29
29
|
script_encoding = opts[:error_description].encoding
|
data/lib/oauth2/mac_token.rb
CHANGED
@@ -95,16 +95,22 @@ module OAuth2
|
|
95
95
|
#
|
96
96
|
# @param [String] alg the algorithm to use (one of 'hmac-sha-1', 'hmac-sha-256')
|
97
97
|
def algorithm=(alg)
|
98
|
-
@algorithm =
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
98
|
+
@algorithm = case alg.to_s
|
99
|
+
when 'hmac-sha-1'
|
100
|
+
begin
|
101
|
+
OpenSSL::Digest('SHA1').new
|
102
|
+
rescue StandardError
|
103
|
+
OpenSSL::Digest.new('SHA1')
|
104
|
+
end
|
105
|
+
when 'hmac-sha-256'
|
106
|
+
begin
|
107
|
+
OpenSSL::Digest('SHA256').new
|
108
|
+
rescue StandardError
|
109
|
+
OpenSSL::Digest.new('SHA256')
|
110
|
+
end
|
111
|
+
else
|
112
|
+
raise(ArgumentError, 'Unsupported algorithm')
|
113
|
+
end
|
108
114
|
end
|
109
115
|
|
110
116
|
private
|
data/lib/oauth2/response.rb
CHANGED
@@ -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
|
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
|
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
|
53
|
+
:grant_type => 'assertion',
|
54
54
|
:assertion_type => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
|
55
|
-
:assertion
|
56
|
-
: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'
|
22
|
-
'password'
|
21
|
+
'username' => username,
|
22
|
+
'password' => password}.merge(params)
|
23
23
|
@client.get_token(params, opts)
|
24
24
|
end
|
25
25
|
end
|
data/lib/oauth2/version.rb
CHANGED
@@ -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
|
-
|
27
|
+
8
|
24
28
|
end
|
25
29
|
|
26
30
|
# The pre-release version, if any
|
27
31
|
#
|
28
|
-
# @return [
|
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
|
-
|
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,30 @@
|
|
1
|
+
DEBUG = ENV['DEBUG'] == 'true'
|
2
|
+
|
3
|
+
ruby_version = Gem::Version.new(RUBY_VERSION)
|
4
|
+
minimum_version = ->(version) { ruby_version >= Gem::Version.new(version) && RUBY_ENGINE == 'ruby' }
|
5
|
+
coverage = minimum_version.call('2.7')
|
6
|
+
debug = minimum_version.call('2.5')
|
7
|
+
|
8
|
+
require 'simplecov' if coverage
|
9
|
+
require 'byebug' if DEBUG && debug
|
10
|
+
|
11
|
+
require 'oauth2'
|
12
|
+
require 'addressable/uri'
|
13
|
+
require 'rspec'
|
14
|
+
require 'rspec/stubbed_env'
|
15
|
+
require 'rspec/pending_for'
|
16
|
+
require 'silent_stream'
|
17
|
+
|
18
|
+
RSpec.configure do |config|
|
19
|
+
config.expect_with :rspec do |c|
|
20
|
+
c.syntax = :expect
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
Faraday.default_adapter = :test
|
25
|
+
|
26
|
+
RSpec.configure do |conf|
|
27
|
+
conf.include SilentStream
|
28
|
+
end
|
29
|
+
|
30
|
+
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
|