omniauth-duodealer-oauth2 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|