omniauth-duodealer-oauth2 1.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.
- checksums.yaml +7 -0
- data/.github/probots.yml +2 -0
- data/.gitignore +2 -0
- data/.ruby-version +1 -0
- data/.travis.yml +4 -0
- data/Gemfile +9 -0
- data/Rakefile +9 -0
- data/example/Gemfile +8 -0
- data/example/config.ru +67 -0
- data/lib/omniauth-duodealer-oauth2.rb +1 -0
- data/lib/omniauth/duodealer.rb +4 -0
- data/lib/omniauth/duodealer/version.rb +7 -0
- data/lib/omniauth/strategies/duodealer.rb +160 -0
- data/omniauth-duodealer-oauth2.gemspec +26 -0
- data/shipit.rubygems.yml +1 -0
- data/spec/omniauth/strategies/duodealer_spec.rb +145 -0
- data/test/integration_test.rb +454 -0
- data/test/test_helper.rb +11 -0
- metadata +119 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 7ff6856a264f3b30185124b802f35d4b48a305455ec9cc0346c5cb52e809fd05
|
4
|
+
data.tar.gz: d7d8d48b5f7055c0c680ee73ddfd6416fff76e76f9b45ec01bd1a5297ab74c72
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 680c5d8f690f20e308479c16741f58dcb7c99a840ce4b97a0f0d125dd62bad985d494c306f599e450359f52a2716aa73e0b1caec6f9f44dccb57b89a451449d8
|
7
|
+
data.tar.gz: df3498079be1c82c0f76742be0beae204f784e7736aa31be3d1469f5a042ca6534619e639d7cda66c22b1746b0096ca8f698440bf88116f6ae3d6b4e88af807e
|
data/.github/probots.yml
ADDED
data/.gitignore
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.6.4
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Rakefile
ADDED
data/example/Gemfile
ADDED
data/example/config.ru
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
require 'sinatra/base'
|
3
|
+
require 'omniauth-duodealer-oauth2'
|
4
|
+
|
5
|
+
SCOPE = 'read_products,read_orders,read_customers,write_shipping'
|
6
|
+
DUODEALER_API_KEY = ENV['DUODEALER_API_KEY']
|
7
|
+
DUODEALER_SHARED_SECRET = ENV['DUODEALER_SHARED_SECRET']
|
8
|
+
|
9
|
+
unless DUODEALER_API_KEY && DUODEALER_SHARED_SECRET
|
10
|
+
abort("DUODEALER_API_KEY and DUODEALER_SHARED_SECRET environment variables must be set")
|
11
|
+
end
|
12
|
+
|
13
|
+
class App < Sinatra::Base
|
14
|
+
get '/' do
|
15
|
+
<<-HTML
|
16
|
+
<html>
|
17
|
+
<head>
|
18
|
+
<title>DuoDealer Oauth2</title>
|
19
|
+
</head>
|
20
|
+
<body>
|
21
|
+
<form action="/auth/duodealer" method="get">
|
22
|
+
<label for="shop">Enter your store's URL:</label>
|
23
|
+
<input type="text" name="shop" placeholder="your-shop-url.duodealer.com">
|
24
|
+
<button type="submit">Log In</button>
|
25
|
+
</form>
|
26
|
+
</body>
|
27
|
+
</html>
|
28
|
+
HTML
|
29
|
+
end
|
30
|
+
|
31
|
+
get '/auth/:provider/callback' do
|
32
|
+
<<-HTML
|
33
|
+
<html>
|
34
|
+
<head>
|
35
|
+
<title>DuoDealer Oauth2</title>
|
36
|
+
</head>
|
37
|
+
<body>
|
38
|
+
<h3>Authorized</h3>
|
39
|
+
<p>Shop: #{request.env['omniauth.auth'].uid}</p>
|
40
|
+
<p>Token: #{request.env['omniauth.auth']['credentials']['token']}</p>
|
41
|
+
</body>
|
42
|
+
</html>
|
43
|
+
HTML
|
44
|
+
end
|
45
|
+
|
46
|
+
get '/auth/failure' do
|
47
|
+
<<-HTML
|
48
|
+
<html>
|
49
|
+
<head>
|
50
|
+
<title>DuoDealer Oauth2</title>
|
51
|
+
</head>
|
52
|
+
<body>
|
53
|
+
<h3>Failed Authorization</h3>
|
54
|
+
<p>Message: #{params[:message]}</p>
|
55
|
+
</body>
|
56
|
+
</html>
|
57
|
+
HTML
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
use Rack::Session::Cookie, secret: SecureRandom.hex(64)
|
62
|
+
|
63
|
+
use OmniAuth::Builder do
|
64
|
+
provider :duodealer, DUODEALER_API_KEY, DUODEALER_SHARED_SECRET, :scope => SCOPE
|
65
|
+
end
|
66
|
+
|
67
|
+
run App.new
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'omniauth/duodealer'
|
@@ -0,0 +1,160 @@
|
|
1
|
+
require 'omniauth/strategies/oauth2'
|
2
|
+
|
3
|
+
module OmniAuth
|
4
|
+
module Strategies
|
5
|
+
class Duodealer < OmniAuth::Strategies::OAuth2
|
6
|
+
# Available scopes: content themes products customers orders script_tags shipping
|
7
|
+
# read_* or write_*
|
8
|
+
DEFAULT_SCOPE = 'read_products'
|
9
|
+
SCOPE_DELIMITER = ','
|
10
|
+
MINUTE = 60
|
11
|
+
CODE_EXPIRES_AFTER = 10 * MINUTE
|
12
|
+
|
13
|
+
option :name, "duodealer"
|
14
|
+
|
15
|
+
option :client_options, {
|
16
|
+
:authorize_url => '/admin/oauth/authorize',
|
17
|
+
:token_url => '/admin/oauth/access_token'
|
18
|
+
}
|
19
|
+
|
20
|
+
option :callback_url
|
21
|
+
option :duodealer_domain, 'duodealer.com'
|
22
|
+
option :old_client_secret
|
23
|
+
|
24
|
+
# When `true`, the user's permission level will apply (in addition to
|
25
|
+
# the requested access scope) when making API requests to duodealer.
|
26
|
+
option :per_user_permissions, false
|
27
|
+
|
28
|
+
option :setup, proc { |env|
|
29
|
+
strategy = env['omniauth.strategy']
|
30
|
+
|
31
|
+
duodealer_auth_params = strategy.session['duodealer.omniauth_params'] && strategy.session['duodealer.omniauth_params'].with_indifferent_access
|
32
|
+
shop = if duodealer_auth_params && duodealer_auth_params['shop']
|
33
|
+
"https://#{duodealer_auth_params['shop']}"
|
34
|
+
else
|
35
|
+
''
|
36
|
+
end
|
37
|
+
|
38
|
+
strategy.options[:client_options][:site] = shop
|
39
|
+
}
|
40
|
+
|
41
|
+
uid { URI.parse(options[:client_options][:site]).host }
|
42
|
+
|
43
|
+
extra do
|
44
|
+
if access_token
|
45
|
+
{
|
46
|
+
'associated_user' => access_token['associated_user'],
|
47
|
+
'associated_user_scope' => access_token['associated_user_scope'],
|
48
|
+
'scope' => access_token['scope'],
|
49
|
+
'session' => access_token['session']
|
50
|
+
}
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def valid_site?
|
55
|
+
!!(/\A(https|http)\:\/\/[a-zA-Z0-9][a-zA-Z0-9\-]*\.#{Regexp.quote(options[:duodealer_domain])}[\/]?\z/ =~ options[:client_options][:site])
|
56
|
+
end
|
57
|
+
|
58
|
+
def valid_signature?
|
59
|
+
return false unless request.POST.empty?
|
60
|
+
|
61
|
+
params = request.GET
|
62
|
+
signature = params['hmac']
|
63
|
+
timestamp = params['timestamp']
|
64
|
+
return false unless signature && timestamp
|
65
|
+
|
66
|
+
return false unless timestamp.to_i > Time.now.to_i - CODE_EXPIRES_AFTER
|
67
|
+
|
68
|
+
new_secret = options.client_secret
|
69
|
+
old_secret = options.old_client_secret
|
70
|
+
|
71
|
+
validate_signature(new_secret) || (old_secret && validate_signature(old_secret))
|
72
|
+
end
|
73
|
+
|
74
|
+
def valid_scope?(token)
|
75
|
+
params = options.authorize_params.merge(options_for("authorize"))
|
76
|
+
return false unless token && params[:scope] && token['scope']
|
77
|
+
expected_scope = normalized_scopes(params[:scope]).sort
|
78
|
+
(expected_scope == token['scope'].split(SCOPE_DELIMITER).sort)
|
79
|
+
end
|
80
|
+
|
81
|
+
def normalized_scopes(scopes)
|
82
|
+
scope_list = scopes.to_s.split(SCOPE_DELIMITER).map(&:strip).reject(&:empty?).uniq
|
83
|
+
ignore_scopes = scope_list.map { |scope| scope =~ /\Awrite_(.*)\z/ && "read_#{$1}" }.compact
|
84
|
+
scope_list - ignore_scopes
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.encoded_params_for_signature(params)
|
88
|
+
params = params.dup
|
89
|
+
params.delete('hmac')
|
90
|
+
params.delete('signature') # deprecated signature
|
91
|
+
Rack::Utils.build_query(params.sort)
|
92
|
+
end
|
93
|
+
|
94
|
+
def self.hmac_sign(encoded_params, secret)
|
95
|
+
OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, secret, encoded_params)
|
96
|
+
end
|
97
|
+
|
98
|
+
def valid_permissions?(token)
|
99
|
+
token && (options[:per_user_permissions] == !token['associated_user'].nil?)
|
100
|
+
end
|
101
|
+
|
102
|
+
def fix_https
|
103
|
+
options[:client_options][:site] = options[:client_options][:site].gsub(/\Ahttp\:/, 'https:')
|
104
|
+
end
|
105
|
+
|
106
|
+
def setup_phase
|
107
|
+
super
|
108
|
+
fix_https
|
109
|
+
end
|
110
|
+
|
111
|
+
def request_phase
|
112
|
+
if valid_site?
|
113
|
+
super
|
114
|
+
else
|
115
|
+
fail!(:invalid_site)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def callback_phase
|
120
|
+
return fail!(:invalid_site, CallbackError.new(:invalid_site, "OAuth endpoint is not a duodealer site.")) unless valid_site?
|
121
|
+
return fail!(:invalid_signature, CallbackError.new(:invalid_signature, "Signature does not match, it may have been tampered with.")) unless valid_signature?
|
122
|
+
|
123
|
+
token = build_access_token
|
124
|
+
unless valid_scope?(token)
|
125
|
+
return fail!(:invalid_scope, CallbackError.new(:invalid_scope, "Scope does not match, it may have been tampered with."))
|
126
|
+
end
|
127
|
+
unless valid_permissions?(token)
|
128
|
+
return fail!(:invalid_permissions, CallbackError.new(:invalid_permissions, "Requested API access mode does not match."))
|
129
|
+
end
|
130
|
+
|
131
|
+
super
|
132
|
+
rescue ::OAuth2::Error => e
|
133
|
+
fail!(:invalid_credentials, e)
|
134
|
+
end
|
135
|
+
|
136
|
+
def build_access_token
|
137
|
+
@built_access_token ||= super
|
138
|
+
end
|
139
|
+
|
140
|
+
def authorize_params
|
141
|
+
super.tap do |params|
|
142
|
+
params[:scope] = normalized_scopes(params[:scope] || DEFAULT_SCOPE).join(SCOPE_DELIMITER)
|
143
|
+
params[:grant_options] = ['per-user'] if options[:per_user_permissions]
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def callback_url
|
148
|
+
options[:callback_url] || full_host + script_name + callback_path
|
149
|
+
end
|
150
|
+
|
151
|
+
private
|
152
|
+
|
153
|
+
def validate_signature(secret)
|
154
|
+
params = request.GET
|
155
|
+
calculated_signature = self.class.hmac_sign(self.class.encoded_params_for_signature(params), secret)
|
156
|
+
Rack::Utils.secure_compare(calculated_signature, params['hmac'])
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
$LOAD_PATH.push File.expand_path('lib', __dir__)
|
4
|
+
require 'omniauth/duodealer/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = 'omniauth-duodealer-oauth2'
|
8
|
+
s.version = OmniAuth::Duodealer::VERSION
|
9
|
+
s.authors = ['Eric Raio']
|
10
|
+
s.email = ['eric@duodealer.com']
|
11
|
+
s.summary = 'duodealer strategy for OmniAuth'
|
12
|
+
s.homepage = 'https://gitlab.com/duodealer/omniauth-duodealer-oauth2'
|
13
|
+
s.license = 'MIT'
|
14
|
+
|
15
|
+
s.files = `git ls-files`.split("\n")
|
16
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
17
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
|
18
|
+
s.require_paths = ['lib']
|
19
|
+
s.required_ruby_version = '>= 2.1.9'
|
20
|
+
|
21
|
+
s.add_runtime_dependency 'activesupport'
|
22
|
+
s.add_runtime_dependency 'omniauth-oauth2', '~> 1.5.0'
|
23
|
+
|
24
|
+
s.add_development_dependency 'minitest', '~> 5.6'
|
25
|
+
s.add_development_dependency 'rake'
|
26
|
+
end
|
data/shipit.rubygems.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# using the default shipit config
|
@@ -0,0 +1,145 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
require 'omniauth-duodealer-oauth2'
|
5
|
+
require 'base64'
|
6
|
+
|
7
|
+
describe OmniAuth::Strategies::Duodealer do
|
8
|
+
before :each do
|
9
|
+
@request = double('Request',
|
10
|
+
env: {})
|
11
|
+
@request.stub(:params) { {} }
|
12
|
+
@request.stub(:cookies) { {} }
|
13
|
+
|
14
|
+
@client_id = '123'
|
15
|
+
@client_secret = '53cr3tz'
|
16
|
+
@options = { client_options: { site: 'https://example.duodealer.com' } }
|
17
|
+
end
|
18
|
+
|
19
|
+
subject do
|
20
|
+
args = [@client_id, @client_secret, @options].compact
|
21
|
+
OmniAuth::Strategies::Duodealer.new(nil, *args).tap do |strategy|
|
22
|
+
strategy.stub(:request) { @request }
|
23
|
+
strategy.stub(:session) { {} }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe '#fix_https' do
|
28
|
+
it 'replaces http scheme by https' do
|
29
|
+
@options = { client_options: { site: 'http://foo.bar/' } }
|
30
|
+
subject.fix_https
|
31
|
+
subject.options[:client_options][:site].should eq('https://foo.bar/')
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'replaces http scheme by https with an immutable string' do
|
35
|
+
@options = { client_options: { site: 'http://foo.bar/' } }
|
36
|
+
subject.fix_https
|
37
|
+
subject.options[:client_options][:site].should eq('https://foo.bar/')
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'does not replace https scheme' do
|
41
|
+
@options = { client_options: { site: 'https://foo.bar/' } }
|
42
|
+
subject.fix_https
|
43
|
+
subject.options[:client_options][:site].should eq('https://foo.bar/')
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe '#client' do
|
48
|
+
it 'has correct duodealer site' do
|
49
|
+
subject.client.site.should eq('https://example.duodealer.com')
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'has correct authorize url' do
|
53
|
+
subject.client.options[:authorize_url].should eq('/admin/oauth/authorize')
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'has correct token url' do
|
57
|
+
subject.client.options[:token_url].should eq('/admin/oauth/access_token')
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
describe '#callback_url' do
|
62
|
+
it 'returns value from #callback_url' do
|
63
|
+
url = 'http://auth.myapp.com/auth/callback'
|
64
|
+
@options = { callback_url: url }
|
65
|
+
subject.callback_url.should eq(url)
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'defaults to callback' do
|
69
|
+
url_base = 'http://auth.request.com'
|
70
|
+
@request.stub(:url) { "#{url_base}/page/path" }
|
71
|
+
@request.stub(:scheme) { 'http' }
|
72
|
+
subject.stub(:script_name) { '' } # to not depend from Rack env
|
73
|
+
subject.callback_url.should eq("#{url_base}/auth/duodealer/callback")
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
describe '#authorize_params' do
|
78
|
+
it 'includes default scope for read_products' do
|
79
|
+
subject.authorize_params.should be_a(Hash)
|
80
|
+
subject.authorize_params[:scope].should eq('read_products')
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'includes custom scope' do
|
84
|
+
@options = { scope: 'write_products' }
|
85
|
+
subject.authorize_params.should be_a(Hash)
|
86
|
+
subject.authorize_params[:scope].should eq('write_products')
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
describe '#uid' do
|
91
|
+
it 'returns the shop' do
|
92
|
+
subject.uid.should eq('example.duodealer.com')
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
describe '#credentials' do
|
97
|
+
before :each do
|
98
|
+
@access_token = double('OAuth2::AccessToken')
|
99
|
+
@access_token.stub(:token)
|
100
|
+
@access_token.stub(:expires?)
|
101
|
+
@access_token.stub(:expires_at)
|
102
|
+
@access_token.stub(:refresh_token)
|
103
|
+
subject.stub(:access_token) { @access_token }
|
104
|
+
end
|
105
|
+
|
106
|
+
it 'returns a Hash' do
|
107
|
+
subject.credentials.should be_a(Hash)
|
108
|
+
end
|
109
|
+
|
110
|
+
it 'returns the token' do
|
111
|
+
@access_token.stub(:token) { '123' }
|
112
|
+
subject.credentials['token'].should eq('123')
|
113
|
+
end
|
114
|
+
|
115
|
+
it 'returns the expiry status' do
|
116
|
+
@access_token.stub(:expires?) { true }
|
117
|
+
subject.credentials['expires'].should eq(true)
|
118
|
+
|
119
|
+
@access_token.stub(:expires?) { false }
|
120
|
+
subject.credentials['expires'].should eq(false)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
describe '#valid_site?' do
|
125
|
+
it 'returns true if the site contains .duodealer.com' do
|
126
|
+
@options = { client_options: { site: 'http://foo.duodealer.com/' } }
|
127
|
+
subject.valid_site?.should eq(true)
|
128
|
+
end
|
129
|
+
|
130
|
+
it 'returns false if the site does not contain .duodealer.com' do
|
131
|
+
@options = { client_options: { site: 'http://foo.example.com/' } }
|
132
|
+
subject.valid_site?.should eq(false)
|
133
|
+
end
|
134
|
+
|
135
|
+
it 'uses configurable option for duodealer_domain' do
|
136
|
+
@options = { client_options: { site: 'http://foo.example.com/' }, duodealer_domain: 'example.com' }
|
137
|
+
subject.valid_site?.should eq(true)
|
138
|
+
end
|
139
|
+
|
140
|
+
it 'allows custom port for duodealer_domain' do
|
141
|
+
@options = { client_options: { site: 'http://foo.example.com:3456/' }, duodealer_domain: 'example.com:3456' }
|
142
|
+
subject.valid_site?.should eq(true)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,454 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
class IntegrationTest < Minitest::Test
|
4
|
+
def setup
|
5
|
+
build_app(scope: OmniAuth::Strategies::Duodealer::DEFAULT_SCOPE)
|
6
|
+
end
|
7
|
+
|
8
|
+
def teardown
|
9
|
+
FakeWeb.clean_registry
|
10
|
+
FakeWeb.last_request = nil
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_authorize
|
14
|
+
response = authorize('snowdevil.duodealer.com')
|
15
|
+
assert_equal 302, response.status
|
16
|
+
assert_match %r{\A#{Regexp.quote(duodealer_authorize_url)}}, response.location
|
17
|
+
redirect_params = Rack::Utils.parse_query(URI(response.location).query)
|
18
|
+
assert_equal "123", redirect_params['client_id']
|
19
|
+
assert_equal "https://app.example.com/auth/duodealer/callback", redirect_params['redirect_uri']
|
20
|
+
assert_equal "read_products", redirect_params['scope']
|
21
|
+
assert_nil redirect_params['grant_options']
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_authorize_includes_auth_type_when_per_user_permissions_are_requested
|
25
|
+
build_app(per_user_permissions: true)
|
26
|
+
response = authorize('snowdevil.duodealer.com')
|
27
|
+
redirect_params = Rack::Utils.parse_query(URI(response.location).query)
|
28
|
+
assert_equal 'per-user', redirect_params['grant_options[]']
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_authorize_overrides_site_with_https_scheme
|
32
|
+
build_app setup: lambda { |env|
|
33
|
+
params = Rack::Utils.parse_query(env['QUERY_STRING'])
|
34
|
+
env['omniauth.strategy'].options[:client_options][:site] = "http://#{params['shop']}"
|
35
|
+
}
|
36
|
+
|
37
|
+
response = request.get('https://app.example.com/auth/duodealer?shop=snowdevil.duodealer.com')
|
38
|
+
assert_match %r{\A#{Regexp.quote(duodealer_authorize_url)}}, response.location
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_site_validation
|
42
|
+
code = SecureRandom.hex(16)
|
43
|
+
|
44
|
+
[
|
45
|
+
'foo.example.com', # shop doesn't end with .duodealer.com
|
46
|
+
'http://snowdevil.duodealer.com', # shop contains protocol
|
47
|
+
'snowdevil.duodealer.com/path', # shop contains path
|
48
|
+
'user@snowdevil.duodealer.com', # shop contains user
|
49
|
+
'snowdevil.duodealer.com:22', # shop contains port
|
50
|
+
].each do |shop, valid|
|
51
|
+
@shop = shop
|
52
|
+
response = authorize(shop)
|
53
|
+
assert_auth_failure(response, 'invalid_site')
|
54
|
+
|
55
|
+
response = callback(sign_with_new_secret(shop: shop, code: code))
|
56
|
+
assert_auth_failure(response, 'invalid_site')
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def test_callback
|
61
|
+
access_token = SecureRandom.hex(16)
|
62
|
+
code = SecureRandom.hex(16)
|
63
|
+
expect_access_token_request(access_token, OmniAuth::Strategies::Duodealer::DEFAULT_SCOPE)
|
64
|
+
|
65
|
+
response = callback(sign_with_new_secret(shop: 'snowdevil.duodealer.com', code: code, state: opts["rack.session"]["omniauth.state"]))
|
66
|
+
|
67
|
+
assert_callback_success(response, access_token, code)
|
68
|
+
end
|
69
|
+
|
70
|
+
def test_callback_with_legacy_signature
|
71
|
+
build_app scope: OmniAuth::Strategies::Duodealer::DEFAULT_SCOPE
|
72
|
+
access_token = SecureRandom.hex(16)
|
73
|
+
code = SecureRandom.hex(16)
|
74
|
+
expect_access_token_request(access_token, OmniAuth::Strategies::Duodealer::DEFAULT_SCOPE)
|
75
|
+
|
76
|
+
response = callback(sign_with_new_secret(shop: 'snowdevil.duodealer.com', code: code, state: opts["rack.session"]["omniauth.state"]).merge(signature: 'ignored'))
|
77
|
+
|
78
|
+
assert_callback_success(response, access_token, code)
|
79
|
+
end
|
80
|
+
|
81
|
+
def test_callback_custom_params
|
82
|
+
access_token = SecureRandom.hex(16)
|
83
|
+
code = SecureRandom.hex(16)
|
84
|
+
|
85
|
+
expect_access_token_request(access_token, OmniAuth::Strategies::Duodealer::DEFAULT_SCOPE)
|
86
|
+
|
87
|
+
now = Time.now.to_i
|
88
|
+
params = { shop: 'snowdevil.duodealer.com', code: code, timestamp: now, next: '/products?page=2&q=red%20shirt', state: opts["rack.session"]["omniauth.state"] }
|
89
|
+
encoded_params = "code=#{code}&next=%2Fproducts%3Fpage%3D2%26q%3Dred%2520shirt&shop=snowdevil.duodealer.com&state=#{opts["rack.session"]["omniauth.state"]}×tamp=#{now}"
|
90
|
+
params[:hmac] = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, @secret, encoded_params)
|
91
|
+
|
92
|
+
response = callback(params)
|
93
|
+
|
94
|
+
assert_callback_success(response, access_token, code)
|
95
|
+
end
|
96
|
+
|
97
|
+
def test_callback_with_spaces_in_scope
|
98
|
+
build_app scope: 'write_products, read_orders'
|
99
|
+
access_token = SecureRandom.hex(16)
|
100
|
+
code = SecureRandom.hex(16)
|
101
|
+
expect_access_token_request(access_token, 'read_orders,write_products')
|
102
|
+
|
103
|
+
response = callback(sign_with_new_secret(shop: 'snowdevil.duodealer.com', code: code, state: opts["rack.session"]["omniauth.state"]))
|
104
|
+
|
105
|
+
assert_callback_success(response, access_token, code)
|
106
|
+
end
|
107
|
+
|
108
|
+
def test_callback_rejects_invalid_hmac
|
109
|
+
@secret = 'wrong_secret'
|
110
|
+
response = callback(sign_with_new_secret(shop: 'snowdevil.duodealer.com', code: SecureRandom.hex(16)))
|
111
|
+
|
112
|
+
assert_auth_failure(response, 'invalid_signature')
|
113
|
+
end
|
114
|
+
|
115
|
+
def test_callback_rejects_old_timestamps
|
116
|
+
expired_timestamp = Time.now.to_i - OmniAuth::Strategies::Duodealer::CODE_EXPIRES_AFTER - 1
|
117
|
+
response = callback(sign_with_new_secret(shop: 'snowdevil.duodealer.com', code: SecureRandom.hex(16), timestamp: expired_timestamp))
|
118
|
+
|
119
|
+
assert_auth_failure(response, 'invalid_signature')
|
120
|
+
end
|
121
|
+
|
122
|
+
def test_callback_rejects_missing_hmac
|
123
|
+
code = SecureRandom.hex(16)
|
124
|
+
|
125
|
+
response = callback(shop: 'snowdevil.duodealer.com', code: code, timestamp: Time.now.to_i)
|
126
|
+
|
127
|
+
assert_auth_failure(response, 'invalid_signature')
|
128
|
+
end
|
129
|
+
|
130
|
+
def test_callback_rejects_body_params
|
131
|
+
code = SecureRandom.hex(16)
|
132
|
+
params = sign_with_new_secret(shop: 'snowdevil.duodealer.com', code: code)
|
133
|
+
body = Rack::Utils.build_nested_query(unsigned: 'value')
|
134
|
+
|
135
|
+
response = request.get("https://app.example.com/auth/duodealer/callback?#{Rack::Utils.build_query(params)}",
|
136
|
+
input: body,
|
137
|
+
"CONTENT_TYPE" => 'application/x-www-form-urlencoded',
|
138
|
+
'rack.session' => {
|
139
|
+
'duodealer.omniauth_params' => { shop: 'snowdevil.duodealer.com' }
|
140
|
+
})
|
141
|
+
|
142
|
+
assert_auth_failure(response, 'invalid_signature')
|
143
|
+
end
|
144
|
+
|
145
|
+
def test_provider_options
|
146
|
+
build_app scope: 'read_products,read_orders,write_content',
|
147
|
+
callback_path: '/admin/auth/legacy/callback',
|
148
|
+
duodealer_domain: 'duodealer.dev:3000',
|
149
|
+
setup: lambda { |env|
|
150
|
+
shop = Rack::Request.new(env).GET['shop']
|
151
|
+
shop += ".duodealer.dev:3000" unless shop.include?(".")
|
152
|
+
env['omniauth.strategy'].options[:client_options][:site] = "https://#{shop}"
|
153
|
+
}
|
154
|
+
|
155
|
+
response = request.get("https://app.example.com/auth/duodealer?shop=snowdevil.duodealer.dev:3000")
|
156
|
+
assert_equal 302, response.status
|
157
|
+
assert_match %r{\A#{Regexp.quote("https://snowdevil.duodealer.dev:3000/admin/oauth/authorize?")}}, response.location
|
158
|
+
redirect_params = Rack::Utils.parse_query(URI(response.location).query)
|
159
|
+
assert_equal 'read_products,read_orders,write_content', redirect_params['scope']
|
160
|
+
assert_equal 'https://app.example.com/admin/auth/legacy/callback', redirect_params['redirect_uri']
|
161
|
+
end
|
162
|
+
|
163
|
+
def test_default_setup_reads_shop_from_session
|
164
|
+
build_app
|
165
|
+
response = authorize('snowdevil.duodealer.com')
|
166
|
+
assert_equal 302, response.status
|
167
|
+
assert_match %r{\A#{Regexp.quote("https://snowdevil.duodealer.com/admin/oauth/authorize?")}}, response.location
|
168
|
+
redirect_params = Rack::Utils.parse_query(URI(response.location).query)
|
169
|
+
assert_equal 'https://app.example.com/auth/duodealer/callback', redirect_params['redirect_uri']
|
170
|
+
end
|
171
|
+
|
172
|
+
def test_unnecessary_read_scopes_are_removed
|
173
|
+
build_app scope: 'read_content,read_products,write_products',
|
174
|
+
callback_path: '/admin/auth/legacy/callback',
|
175
|
+
duodealer_domain: 'duodealer.dev:3000',
|
176
|
+
setup: lambda { |env|
|
177
|
+
shop = Rack::Request.new(env).GET['shop']
|
178
|
+
env['omniauth.strategy'].options[:client_options][:site] = "https://#{shop}"
|
179
|
+
}
|
180
|
+
|
181
|
+
response = request.get("https://app.example.com/auth/duodealer?shop=snowdevil.duodealer.dev:3000")
|
182
|
+
assert_equal 302, response.status
|
183
|
+
redirect_params = Rack::Utils.parse_query(URI(response.location).query)
|
184
|
+
assert_equal 'read_content,write_products', redirect_params['scope']
|
185
|
+
end
|
186
|
+
|
187
|
+
def test_callback_with_invalid_state_fails
|
188
|
+
access_token = SecureRandom.hex(16)
|
189
|
+
code = SecureRandom.hex(16)
|
190
|
+
expect_access_token_request(access_token, OmniAuth::Strategies::Duodealer::DEFAULT_SCOPE)
|
191
|
+
|
192
|
+
response = callback(sign_with_new_secret(shop: 'snowdevil.duodealer.com', code: code, state: 'invalid'))
|
193
|
+
|
194
|
+
assert_equal 302, response.status
|
195
|
+
assert_equal '/auth/failure?message=csrf_detected&strategy=duodealer', response.location
|
196
|
+
end
|
197
|
+
|
198
|
+
def test_callback_with_mismatching_scope_fails
|
199
|
+
access_token = SecureRandom.hex(16)
|
200
|
+
code = SecureRandom.hex(16)
|
201
|
+
expect_access_token_request(access_token, 'some_invalid_scope', nil)
|
202
|
+
|
203
|
+
response = callback(sign_with_new_secret(shop: 'snowdevil.duodealer.com', code: code, state: opts["rack.session"]["omniauth.state"]))
|
204
|
+
|
205
|
+
assert_equal 302, response.status
|
206
|
+
assert_equal '/auth/failure?message=invalid_scope&strategy=duodealer', response.location
|
207
|
+
end
|
208
|
+
|
209
|
+
def test_callback_with_no_scope_fails
|
210
|
+
access_token = SecureRandom.hex(16)
|
211
|
+
code = SecureRandom.hex(16)
|
212
|
+
expect_access_token_request(access_token, nil)
|
213
|
+
|
214
|
+
response = callback(sign_with_new_secret(shop: 'snowdevil.duodealer.com', code: code, state: opts["rack.session"]["omniauth.state"]))
|
215
|
+
|
216
|
+
assert_equal 302, response.status
|
217
|
+
assert_equal '/auth/failure?message=invalid_scope&strategy=duodealer', response.location
|
218
|
+
end
|
219
|
+
|
220
|
+
def test_callback_with_missing_access_scope_fails
|
221
|
+
build_app scope: 'first_scope,second_scope'
|
222
|
+
|
223
|
+
access_token = SecureRandom.hex(16)
|
224
|
+
code = SecureRandom.hex(16)
|
225
|
+
expect_access_token_request(access_token, 'first_scope')
|
226
|
+
|
227
|
+
response = callback(sign_with_new_secret(shop: 'snowdevil.duodealer.com', code: code, state: opts["rack.session"]["omniauth.state"]))
|
228
|
+
|
229
|
+
assert_equal 302, response.status
|
230
|
+
assert_equal '/auth/failure?message=invalid_scope&strategy=duodealer', response.location
|
231
|
+
end
|
232
|
+
|
233
|
+
def test_callback_with_extra_access_scope_fails
|
234
|
+
build_app scope: 'first_scope,second_scope'
|
235
|
+
|
236
|
+
access_token = SecureRandom.hex(16)
|
237
|
+
code = SecureRandom.hex(16)
|
238
|
+
expect_access_token_request(access_token, 'second_scope,first_scope,third_scope')
|
239
|
+
|
240
|
+
response = callback(sign_with_new_secret(shop: 'snowdevil.duodealer.com', code: code, state: opts["rack.session"]["omniauth.state"]))
|
241
|
+
|
242
|
+
assert_equal 302, response.status
|
243
|
+
assert_equal '/auth/failure?message=invalid_scope&strategy=duodealer', response.location
|
244
|
+
end
|
245
|
+
|
246
|
+
def test_callback_with_scopes_out_of_order_works
|
247
|
+
build_app scope: 'first_scope,second_scope'
|
248
|
+
|
249
|
+
access_token = SecureRandom.hex(16)
|
250
|
+
code = SecureRandom.hex(16)
|
251
|
+
expect_access_token_request(access_token, 'second_scope,first_scope')
|
252
|
+
|
253
|
+
response = callback(sign_with_new_secret(shop: 'snowdevil.duodealer.com', code: code, state: opts["rack.session"]["omniauth.state"]))
|
254
|
+
|
255
|
+
assert_callback_success(response, access_token, code)
|
256
|
+
end
|
257
|
+
|
258
|
+
def test_callback_with_extra_coma_works
|
259
|
+
build_app scope: 'read_content,,write_products,'
|
260
|
+
|
261
|
+
access_token = SecureRandom.hex(16)
|
262
|
+
code = SecureRandom.hex(16)
|
263
|
+
expect_access_token_request(access_token, 'read_content,write_products')
|
264
|
+
|
265
|
+
response = callback(sign_with_new_secret(shop: 'snowdevil.duodealer.com', code: code, state: opts["rack.session"]["omniauth.state"]))
|
266
|
+
|
267
|
+
assert_callback_success(response, access_token, code)
|
268
|
+
end
|
269
|
+
|
270
|
+
def test_callback_when_per_user_permissions_are_present_but_not_requested
|
271
|
+
build_app(scope: 'scope', per_user_permissions: false)
|
272
|
+
|
273
|
+
access_token = SecureRandom.hex(16)
|
274
|
+
code = SecureRandom.hex(16)
|
275
|
+
expect_access_token_request(access_token, 'scope', { id: 1, email: 'bob@bobsen.com'})
|
276
|
+
|
277
|
+
response = callback(sign_with_new_secret(shop: 'snowdevil.duodealer.com', code: code, state: opts["rack.session"]["omniauth.state"]))
|
278
|
+
|
279
|
+
assert_equal 302, response.status
|
280
|
+
assert_equal '/auth/failure?message=invalid_permissions&strategy=duodealer', response.location
|
281
|
+
end
|
282
|
+
|
283
|
+
def test_callback_when_per_user_permissions_are_not_present_but_requested
|
284
|
+
build_app(scope: 'scope', per_user_permissions: true)
|
285
|
+
|
286
|
+
access_token = SecureRandom.hex(16)
|
287
|
+
code = SecureRandom.hex(16)
|
288
|
+
expect_access_token_request(access_token, 'scope', nil)
|
289
|
+
|
290
|
+
response = callback(sign_with_new_secret(shop: 'snowdevil.duodealer.com', code: code, state: opts["rack.session"]["omniauth.state"]))
|
291
|
+
|
292
|
+
assert_equal 302, response.status
|
293
|
+
assert_equal '/auth/failure?message=invalid_permissions&strategy=duodealer', response.location
|
294
|
+
end
|
295
|
+
|
296
|
+
def test_callback_works_when_per_user_permissions_are_present_and_requested
|
297
|
+
build_app(scope: 'scope', per_user_permissions: true)
|
298
|
+
|
299
|
+
access_token = SecureRandom.hex(16)
|
300
|
+
code = SecureRandom.hex(16)
|
301
|
+
expect_access_token_request(access_token, 'scope', { id: 1, email: 'bob@bobsen.com'})
|
302
|
+
|
303
|
+
response = callback(sign_with_new_secret(shop: 'snowdevil.duodealer.com', code: code, state: opts["rack.session"]["omniauth.state"]))
|
304
|
+
|
305
|
+
assert_equal 200, response.status
|
306
|
+
end
|
307
|
+
|
308
|
+
def test_callback_when_a_session_is_present
|
309
|
+
build_app(scope: 'scope', per_user_permissions: true)
|
310
|
+
|
311
|
+
access_token = SecureRandom.hex(16)
|
312
|
+
code = SecureRandom.hex(16)
|
313
|
+
session = SecureRandom.hex
|
314
|
+
expect_access_token_request(access_token, 'scope', { id: 1, email: 'bob@bobsen.com'}, session)
|
315
|
+
|
316
|
+
response = callback(sign_with_new_secret(shop: 'snowdevil.duodealer.com', code: code, state: opts["rack.session"]["omniauth.state"]))
|
317
|
+
|
318
|
+
assert_equal 200, response.status
|
319
|
+
end
|
320
|
+
|
321
|
+
def test_callback_works_with_old_secret
|
322
|
+
build_app scope: OmniAuth::Strategies::Duodealer::DEFAULT_SCOPE
|
323
|
+
access_token = SecureRandom.hex(16)
|
324
|
+
code = SecureRandom.hex(16)
|
325
|
+
expect_access_token_request(access_token, OmniAuth::Strategies::Duodealer::DEFAULT_SCOPE)
|
326
|
+
|
327
|
+
signed_params = sign_with_old_secret(
|
328
|
+
shop: 'snowdevil.duodealer.com',
|
329
|
+
code: code,
|
330
|
+
state: opts["rack.session"]["omniauth.state"]
|
331
|
+
)
|
332
|
+
|
333
|
+
response = callback(signed_params)
|
334
|
+
|
335
|
+
assert_callback_success(response, access_token, code)
|
336
|
+
end
|
337
|
+
|
338
|
+
def test_callback_when_creds_are_invalid
|
339
|
+
build_app scope: OmniAuth::Strategies::Duodealer::DEFAULT_SCOPE
|
340
|
+
|
341
|
+
FakeWeb.register_uri(
|
342
|
+
:post,
|
343
|
+
"https://snowdevil.duodealer.com/admin/oauth/access_token",
|
344
|
+
status: [ "401", "Invalid token" ],
|
345
|
+
body: "Token is invalid or has already been requested"
|
346
|
+
)
|
347
|
+
|
348
|
+
signed_params = sign_with_new_secret(
|
349
|
+
shop: 'snowdevil.duodealer.com',
|
350
|
+
code: SecureRandom.hex(16),
|
351
|
+
state: opts["rack.session"]["omniauth.state"]
|
352
|
+
)
|
353
|
+
|
354
|
+
response = callback(signed_params)
|
355
|
+
|
356
|
+
assert_equal 302, response.status
|
357
|
+
assert_equal '/auth/failure?message=invalid_credentials&strategy=duodealer', response.location
|
358
|
+
end
|
359
|
+
|
360
|
+
private
|
361
|
+
|
362
|
+
def sign_with_old_secret(params)
|
363
|
+
params = add_time(params)
|
364
|
+
encoded_params = OmniAuth::Strategies::Duodealer.encoded_params_for_signature(params)
|
365
|
+
params['hmac'] = OmniAuth::Strategies::Duodealer.hmac_sign(encoded_params, @old_secret)
|
366
|
+
params
|
367
|
+
end
|
368
|
+
|
369
|
+
def add_time(params)
|
370
|
+
params = params.dup
|
371
|
+
params[:timestamp] ||= Time.now.to_i
|
372
|
+
params
|
373
|
+
end
|
374
|
+
|
375
|
+
def sign_with_new_secret(params)
|
376
|
+
params = add_time(params)
|
377
|
+
encoded_params = OmniAuth::Strategies::Duodealer.encoded_params_for_signature(params)
|
378
|
+
params['hmac'] = OmniAuth::Strategies::Duodealer.hmac_sign(encoded_params, @secret)
|
379
|
+
params
|
380
|
+
end
|
381
|
+
|
382
|
+
def expect_access_token_request(access_token, scope, associated_user=nil, session=nil)
|
383
|
+
FakeWeb.register_uri(:post, "https://snowdevil.duodealer.com/admin/oauth/access_token",
|
384
|
+
body: JSON.dump(
|
385
|
+
access_token: access_token,
|
386
|
+
scope: scope,
|
387
|
+
associated_user: associated_user,
|
388
|
+
session: session,
|
389
|
+
),
|
390
|
+
content_type: 'application/json')
|
391
|
+
end
|
392
|
+
|
393
|
+
def assert_callback_success(response, access_token, code)
|
394
|
+
token_request_params = Rack::Utils.parse_query(FakeWeb.last_request.body)
|
395
|
+
assert_equal token_request_params['client_id'], '123'
|
396
|
+
assert_equal token_request_params['client_secret'], @secret
|
397
|
+
assert_equal token_request_params['code'], code
|
398
|
+
|
399
|
+
assert_equal 'snowdevil.duodealer.com', @omniauth_result.uid
|
400
|
+
assert_equal access_token, @omniauth_result.credentials.token
|
401
|
+
assert_equal false, @omniauth_result.credentials.expires
|
402
|
+
|
403
|
+
assert_equal 200, response.status
|
404
|
+
assert_equal "OK", response.body
|
405
|
+
end
|
406
|
+
|
407
|
+
def assert_auth_failure(response, reason)
|
408
|
+
assert_nil FakeWeb.last_request
|
409
|
+
assert_equal 302, response.status
|
410
|
+
assert_match %r{\A#{Regexp.quote("/auth/failure?message=#{reason}")}}, response.location
|
411
|
+
end
|
412
|
+
|
413
|
+
def build_app(options={})
|
414
|
+
@old_secret = '12d34s1'
|
415
|
+
@secret = '53cr3tz'
|
416
|
+
options.merge!(old_client_secret: @old_secret)
|
417
|
+
app = proc { |env|
|
418
|
+
@omniauth_result = env['omniauth.auth']
|
419
|
+
[200, {Rack::CONTENT_TYPE => "text/plain"}, "OK"]
|
420
|
+
}
|
421
|
+
|
422
|
+
opts["rack.session"]["omniauth.state"] = SecureRandom.hex(32)
|
423
|
+
app = OmniAuth::Builder.new(app) do
|
424
|
+
provider :duodealer, '123', '53cr3tz' , options
|
425
|
+
end
|
426
|
+
@app = Rack::Session::Cookie.new(app, secret: SecureRandom.hex(64))
|
427
|
+
end
|
428
|
+
|
429
|
+
def shop
|
430
|
+
@shop ||= 'snowdevil.duodealer.com'
|
431
|
+
end
|
432
|
+
|
433
|
+
def authorize(shop)
|
434
|
+
@opts['rack.session']['duodealer.omniauth_params'] = { shop: shop }
|
435
|
+
request.get('https://app.example.com/auth/duodealer', opts)
|
436
|
+
end
|
437
|
+
|
438
|
+
def callback(params)
|
439
|
+
@opts['rack.session']['duodealer.omniauth_params'] = { shop: shop }
|
440
|
+
request.get("https://app.example.com/auth/duodealer/callback?#{Rack::Utils.build_query(params)}", opts)
|
441
|
+
end
|
442
|
+
|
443
|
+
def opts
|
444
|
+
@opts ||= { "rack.session" => {} }
|
445
|
+
end
|
446
|
+
|
447
|
+
def request
|
448
|
+
Rack::MockRequest.new(@app)
|
449
|
+
end
|
450
|
+
|
451
|
+
def duodealer_authorize_url
|
452
|
+
"https://snowdevil.duodealer.com/admin/oauth/authorize?"
|
453
|
+
end
|
454
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
$: << File.expand_path("../../lib", __FILE__)
|
2
|
+
require 'bundler/setup'
|
3
|
+
require 'omniauth-duodealer-oauth2'
|
4
|
+
|
5
|
+
require 'minitest/autorun'
|
6
|
+
require 'fakeweb'
|
7
|
+
require 'json'
|
8
|
+
require 'active_support/core_ext/hash'
|
9
|
+
|
10
|
+
OmniAuth.config.logger = Logger.new(nil)
|
11
|
+
FakeWeb.allow_net_connect = false
|
metadata
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: omniauth-duodealer-oauth2
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Eric Raio
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-02-22 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: omniauth-oauth2
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.5.0
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 1.5.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: minitest
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '5.6'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '5.6'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description:
|
70
|
+
email:
|
71
|
+
- eric@duodealer.com
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- ".github/probots.yml"
|
77
|
+
- ".gitignore"
|
78
|
+
- ".ruby-version"
|
79
|
+
- ".travis.yml"
|
80
|
+
- Gemfile
|
81
|
+
- Rakefile
|
82
|
+
- example/Gemfile
|
83
|
+
- example/config.ru
|
84
|
+
- lib/omniauth-duodealer-oauth2.rb
|
85
|
+
- lib/omniauth/duodealer.rb
|
86
|
+
- lib/omniauth/duodealer/version.rb
|
87
|
+
- lib/omniauth/strategies/duodealer.rb
|
88
|
+
- omniauth-duodealer-oauth2.gemspec
|
89
|
+
- shipit.rubygems.yml
|
90
|
+
- spec/omniauth/strategies/duodealer_spec.rb
|
91
|
+
- test/integration_test.rb
|
92
|
+
- test/test_helper.rb
|
93
|
+
homepage: https://gitlab.com/duodealer/omniauth-duodealer-oauth2
|
94
|
+
licenses:
|
95
|
+
- MIT
|
96
|
+
metadata: {}
|
97
|
+
post_install_message:
|
98
|
+
rdoc_options: []
|
99
|
+
require_paths:
|
100
|
+
- lib
|
101
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
102
|
+
requirements:
|
103
|
+
- - ">="
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
version: 2.1.9
|
106
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
requirements: []
|
112
|
+
rubygems_version: 3.0.3
|
113
|
+
signing_key:
|
114
|
+
specification_version: 4
|
115
|
+
summary: duodealer strategy for OmniAuth
|
116
|
+
test_files:
|
117
|
+
- spec/omniauth/strategies/duodealer_spec.rb
|
118
|
+
- test/integration_test.rb
|
119
|
+
- test/test_helper.rb
|