omniauth-spiffy-oauth2 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4f7ae17bafcf32f410fdbb57a64db532ca033478ba28f0111364b4b05ab08335
4
+ data.tar.gz: 190f58c4e65a3a9ae6a82b3642de68e98e609bd95f8c7fee06e7f50f39384d14
5
+ SHA512:
6
+ metadata.gz: b7c34f1b05236817a9eca7ae248ebe34492e8a2b8db100295822ec37771b6eb59d88adf8eb204c50000b287e9c6af650d82da95ff80c7acb7df3dcb6dc69093e
7
+ data.tar.gz: 2dd4cdf83954fe9241a9ee106d74b2a92c9090d79c1b53404b9a59c07ca3c7c04a8275d79e3dc9a9cfa68c1d5daf1e3bb39f6887c973f9f94fe51320bfbdd02c
@@ -0,0 +1,2 @@
1
+ pkg/*
2
+ Gemfile.lock
@@ -0,0 +1,7 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.10
4
+ - 2.2.9
5
+ - 2.3.6
6
+ - 2.4.3
7
+ - 2.5.0
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("2.2")
6
+ gem 'rack', '~> 1.6'
7
+ end
@@ -0,0 +1,75 @@
1
+ [![Build Status](https://api.travis-ci.com/SpiffyStores/omniauth-spiffy-oauth2.png?branch=master)](https://travis-ci.com/SpiffyStores/omniauth-spiffy-oauth2)
2
+
3
+ # OmniAuth Spiffy Stores
4
+
5
+ Spiffy Stores OAuth2 Strategy for OmniAuth 1.0.
6
+
7
+ ## Installing
8
+
9
+ Add to your `Gemfile`:
10
+
11
+ ```ruby
12
+ gem 'omniauth-spiffy-oauth2'
13
+ ```
14
+
15
+ Then `bundle install`.
16
+
17
+ ## Usage
18
+
19
+ `OmniAuth::Strategies::Spiffy` is simply a Rack middleware. Read [the OmniAuth 1.0 docs](https://github.com/intridea/omniauth) for detailed instructions.
20
+
21
+ Here's a quick example, adding the middleware to a Rails app in `config/initializers/omniauth.rb`:
22
+
23
+ ```ruby
24
+ Rails.application.config.middleware.use OmniAuth::Builder do
25
+ provider :spiffy, ENV['SPIFFY_STORES_API_KEY'], ENV['SPIFFY_STORES_SHARED_SECRET']
26
+ end
27
+ ```
28
+
29
+ Authenticate the user by having them visit /auth/spiffy with a `store` query parameter of their store's spiffystores.com domain. For example, the following form could be used
30
+
31
+ ```html
32
+ <form action="/auth/spiffy" method="get">
33
+ <label for="shop">Enter your store's URL:</label>
34
+ <input type="text" name="shop" placeholder="your-store-url.spiffystores.com">
35
+ <button type="submit">Log In</button>
36
+ </form>
37
+ ```
38
+
39
+ ## Configuring
40
+
41
+ You can configure the scope, which you pass in to the `provider` method via a `Hash`:
42
+
43
+ * `scope`: A comma-separated list of permissions you want to request from the user. See [the SpiffyStores API docs](https://www.spiffystores.com.au/kb/tutorials_oauth) for a full list of available permissions.
44
+
45
+ For example, to request `read_products`, `read_orders` and `write_content` permissions and display the authentication page:
46
+
47
+ ```ruby
48
+ Rails.application.config.middleware.use OmniAuth::Builder do
49
+ provider :spiffy, ENV['SPIFFY_STORES_API_KEY'], ENV['SPIFFY_STORES_SHARED_SECRET'], :scope => 'read_products,read_orders,write_content'
50
+ end
51
+ ```
52
+
53
+ ## Authentication Hash
54
+
55
+ Here's an example *Authentication Hash* available in `request.env['omniauth.auth']`:
56
+
57
+ ```ruby
58
+ {
59
+ :provider => 'spiffy',
60
+ :uid => 'example.spiffystores.com',
61
+ :credentials => {
62
+ :token => 'afasd923kjh0934kf', # OAuth 2.0 access_token, which you store and use to authenticate API requests
63
+ }
64
+ }
65
+ ```
66
+
67
+ ## License
68
+
69
+ Copyright (c) 2018 by Spiffy Stores
70
+
71
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
72
+
73
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
74
+
75
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,9 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+
4
+ task :default => :test
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.pattern = 'test/**/*_test.rb'
8
+ t.verbose = true
9
+ end
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org/'
2
+
3
+ gem 'rack', '~> 1.6'
4
+
5
+ gem 'sinatra', '~> 1.4'
6
+ gem 'omniauth-spiffy-stores-oauth2', :path => '../'
@@ -0,0 +1,67 @@
1
+ require 'bundler/setup'
2
+ require 'sinatra/base'
3
+ require 'omniauth-spiffy-stores-oauth2'
4
+
5
+ SCOPE = 'read_products,read_orders,read_customers,write_shipping'
6
+ SPIFFY_STORES_API_KEY = ENV['SPIFFY_STORES_API_KEY']
7
+ SPIFFY_STORES_SHARED_SECRET = ENV['SPIFFY_STORES_SHARED_SECRET']
8
+
9
+ unless SPIFFY_STORES_API_KEY && SPIFFY_STORES_SHARED_SECRET
10
+ abort("SPIFFY_STORES_API_KEY and SPIFFY_STORES_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>Spiffy Stores Oauth2</title>
19
+ </head>
20
+ <body>
21
+ <form action="/auth/spiffy_stores" method="get">
22
+ <label for="shop">Enter your store's URL:</label>
23
+ <input type="text" name="shop" placeholder="your-shop-name.spiffystores.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>Spiffy Stores 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>Spiffy Stores 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 :spiffy_stores, SPIFFY_STORES_API_KEY, SPIFFY_STORES_SHARED_SECRET, :scope => SCOPE
65
+ end
66
+
67
+ run App.new
@@ -0,0 +1 @@
1
+ require 'omniauth/spiffy'
@@ -0,0 +1,2 @@
1
+ require 'omniauth/spiffy/version'
2
+ require 'omniauth/strategies/spiffy'
@@ -0,0 +1,5 @@
1
+ module OmniAuth
2
+ module Spiffy
3
+ VERSION = "1.2.1".freeze
4
+ end
5
+ end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+ # encoding: UTF-8
3
+
4
+ require 'omniauth/strategies/oauth2'
5
+
6
+ module OmniAuth
7
+ module Strategies
8
+ class Spiffy < OmniAuth::Strategies::OAuth2
9
+ # Available scopes: content themes products customers orders script_tags shipping
10
+ # read_* or write_*
11
+ DEFAULT_SCOPE = 'read_products'
12
+ SCOPE_DELIMITER = ','
13
+ MINUTE = 60
14
+ CODE_EXPIRES_AFTER = 10 * MINUTE
15
+
16
+ option :client_options, {
17
+ :authorize_url => '/admin/oauth/authorize',
18
+ :token_url => '/admin/oauth/token'
19
+ }
20
+
21
+ option :callback_url
22
+ option :spiffy_stores_domain, 'spiffystores.com'
23
+
24
+ # When `true`, the user's permission level will apply (in addition to
25
+ # the requested access scope) when making API requests to Spiffy Stores.
26
+ option :per_user_permissions, false
27
+
28
+ # When `true`, the user's authorization phase will fail if the granted scopes
29
+ # mismatch the requested scopes.
30
+ option :validate_granted_scopes, true
31
+
32
+ option :setup, proc { |env|
33
+ request = Rack::Request.new(env)
34
+ env['omniauth.strategy'].options[:client_options][:site] = "https://#{request.GET['store']}"
35
+ }
36
+
37
+ uid { URI.parse(options[:client_options][:site]).host }
38
+
39
+ extra do
40
+ if access_token
41
+ {
42
+ 'associated_user' => access_token['associated_user'],
43
+ 'associated_user_scope' => access_token['associated_user_scope'],
44
+ 'scope' => access_token['scope'],
45
+ }
46
+ end
47
+ end
48
+
49
+ def valid_site?
50
+ !!(/\A(https|http)\:\/\/[a-zA-Z0-9][a-zA-Z0-9\-]*\.#{Regexp.quote(options[:spiffy_stores_domain])}[\/]?\z/ =~ options[:client_options][:site])
51
+ end
52
+
53
+ def valid_signature?
54
+ return false unless request.POST.empty?
55
+
56
+ params = request.GET
57
+ signature = params['hmac']
58
+ timestamp = params['timestamp']
59
+ return false unless signature && timestamp
60
+
61
+ return false unless timestamp.to_i > Time.now.to_i - CODE_EXPIRES_AFTER
62
+
63
+ calculated_signature = self.class.hmac_sign(self.class.encoded_params_for_signature(params), options.client_secret)
64
+ Rack::Utils.secure_compare(calculated_signature, signature)
65
+ end
66
+
67
+ def valid_scope?(token)
68
+ params = options.authorize_params.merge(options_for("authorize"))
69
+ return false unless token && params[:scope] && token['scope']
70
+ expected_scope = normalized_scopes(params[:scope]).sort
71
+ (expected_scope == token['scope'].split(SCOPE_DELIMITER).sort)
72
+ end
73
+
74
+ def normalized_scopes(scopes)
75
+ scope_list = scopes.to_s.split(SCOPE_DELIMITER).map(&:strip).reject(&:empty?).uniq
76
+ ignore_scopes = scope_list.map { |scope| scope =~ /\Awrite_(.*)\z/ && "read_#{$1}" }.compact
77
+ scope_list - ignore_scopes
78
+ end
79
+
80
+ def self.encoded_params_for_signature(params)
81
+ params = params.dup
82
+ params.delete('hmac')
83
+ params.delete('signature') # deprecated signature
84
+ params.map{|k,v| "#{URI.escape(k.to_s, '&=%')}=#{URI.escape(v.to_s, '&%')}"}.sort.join('&')
85
+ end
86
+
87
+ def self.hmac_sign(encoded_params, secret)
88
+ OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, secret, encoded_params)
89
+ end
90
+
91
+ def valid_permissions?(token)
92
+ token && (options[:per_user_permissions] == !token['associated_user'].nil?)
93
+ end
94
+
95
+ def fix_https
96
+ options[:client_options][:site] = options[:client_options][:site].gsub(/\Ahttp\:/, 'https:')
97
+ end
98
+
99
+ def setup_phase
100
+ super
101
+ fix_https
102
+ end
103
+
104
+ def request_phase
105
+ if valid_site?
106
+ super
107
+ else
108
+ fail!(:invalid_site)
109
+ end
110
+ end
111
+
112
+ def callback_phase
113
+ return fail!(:invalid_site, CallbackError.new(:invalid_site, "OAuth endpoint is not a Spiffy Stores site.")) unless valid_site?
114
+ return fail!(:invalid_signature, CallbackError.new(:invalid_signature, "Signature does not match, it may have been tampered with.")) unless valid_signature?
115
+
116
+ error = request.params["error_reason"] || request.params["error"]
117
+
118
+ if error
119
+ return fail!(error, CallbackError.new(request.params["error"], request.params["error_description"] || request.params["error_reason"], request.params["error_uri"]))
120
+ else
121
+ token = build_access_token
122
+ unless valid_scope?(token)
123
+ return fail!(:invalid_scope, CallbackError.new(:invalid_scope, "Scope does not match, it may have been tampered with."))
124
+ end
125
+ unless valid_permissions?(token)
126
+ return fail!(:invalid_permissions, CallbackError.new(:invalid_permissions, "Requested API access mode does not match."))
127
+ end
128
+ end
129
+
130
+ super
131
+ end
132
+
133
+ def build_access_token
134
+ @built_access_token ||= super
135
+ end
136
+
137
+ def authorize_params
138
+ super.tap do |params|
139
+ params[:scope] = normalized_scopes(params[:scope] || DEFAULT_SCOPE).join(SCOPE_DELIMITER)
140
+ params[:grant_options] = ['per-user'] if options[:per_user_permissions]
141
+ end
142
+ end
143
+
144
+ def callback_url
145
+ options[:callback_url] || full_host + script_name + callback_path
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path('../lib', __FILE__)
3
+ require 'omniauth/spiffy/version'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'omniauth-spiffy-oauth2'
7
+ s.version = OmniAuth::Spiffy::VERSION
8
+ s.authors = ['Spiffy Stores']
9
+ s.email = ['brian@spiffy.com.au']
10
+ s.summary = 'Spiffy Stores strategy for OmniAuth'
11
+ s.homepage = 'https://github.com/SpiffyStores/omniauth-spiffy-oauth2'
12
+ s.license = 'MIT'
13
+
14
+ s.files = `git ls-files`.split("\n")
15
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
+ s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
17
+ s.require_paths = ['lib']
18
+ s.required_ruby_version = '>= 2.1.9'
19
+
20
+ s.add_runtime_dependency 'omniauth-oauth2', '~> 1.5.0'
21
+
22
+ s.add_development_dependency 'minitest', '~> 5.6'
23
+ s.add_development_dependency 'fakeweb', '~> 1.3'
24
+ s.add_development_dependency 'rake'
25
+ end
@@ -0,0 +1 @@
1
+ # using the default shipit config
@@ -0,0 +1,144 @@
1
+ require 'spec_helper'
2
+ require 'omniauth-spiffy-oauth2'
3
+ require 'base64'
4
+
5
+ describe OmniAuth::Strategies::Spiffy do
6
+ before :each do
7
+ @request = double('Request',
8
+ :env => { })
9
+ @request.stub(:params) { {} }
10
+ @request.stub(:cookies) { {} }
11
+
12
+ @client_id = '123'
13
+ @client_secret = '53cr3tz'
14
+ @options = {:client_options => {:site => 'https://example.spiffystores.com'}}
15
+ end
16
+
17
+ subject do
18
+ args = [@client_id, @client_secret, @options].compact
19
+ OmniAuth::Strategies::SpiffyStores.new(nil, *args).tap do |strategy|
20
+ strategy.stub(:request) { @request }
21
+ strategy.stub(:session) { {} }
22
+ end
23
+ end
24
+
25
+ describe '#fix_https' do
26
+ it 'replaces http scheme by https' do
27
+ @options = {:client_options => {:site => 'http://foo.bar/'}}
28
+ subject.fix_https
29
+ subject.options[:client_options][:site].should eq('https://foo.bar/')
30
+ end
31
+
32
+ it 'replaces http scheme by https with an immutable string' do
33
+ @options = {:client_options => {:site => 'http://foo.bar/'.freeze}}
34
+ subject.fix_https
35
+ subject.options[:client_options][:site].should eq('https://foo.bar/')
36
+ end
37
+
38
+ it 'does not replace https scheme' do
39
+ @options = {:client_options => {:site => 'https://foo.bar/'}}
40
+ subject.fix_https
41
+ subject.options[:client_options][:site].should eq('https://foo.bar/')
42
+ end
43
+ end
44
+
45
+ describe '#client' do
46
+ it 'has correct spiffy_stores site' do
47
+ subject.client.site.should eq('https://example.spiffystores.com')
48
+ end
49
+
50
+ it 'has correct authorize url' do
51
+ subject.client.options[:authorize_url].should eq('/admin/oauth/authorize')
52
+ end
53
+
54
+ it 'has correct token url' do
55
+ subject.client.options[:token_url].should eq('/admin/oauth/access_token')
56
+ end
57
+ end
58
+
59
+ describe '#callback_url' do
60
+ it "returns value from #callback_url" do
61
+ url = 'http://auth.myapp.com/auth/callback'
62
+ @options = {:callback_url => url}
63
+ subject.callback_url.should eq(url)
64
+ end
65
+
66
+ it "defaults to callback" do
67
+ url_base = 'http://auth.request.com'
68
+ @request.stub(:url) { "#{url_base}/page/path" }
69
+ @request.stub(:scheme) { 'http' }
70
+ subject.stub(:script_name) { "" } # to not depend from Rack env
71
+ subject.callback_url.should eq("#{url_base}/auth/spiffy_stores/callback")
72
+ end
73
+ end
74
+
75
+ describe '#authorize_params' do
76
+ it 'includes default scope for read_products' do
77
+ subject.authorize_params.should be_a(Hash)
78
+ subject.authorize_params[:scope].should eq('read_products')
79
+ end
80
+
81
+ it 'includes custom scope' do
82
+ @options = {:scope => 'write_products'}
83
+ subject.authorize_params.should be_a(Hash)
84
+ subject.authorize_params[:scope].should eq('write_products')
85
+ end
86
+ end
87
+
88
+ describe '#uid' do
89
+ it 'returns the shop' do
90
+ subject.uid.should eq('example.spiffystores.com')
91
+ end
92
+ end
93
+
94
+ describe '#credentials' do
95
+ before :each do
96
+ @access_token = double('OAuth2::AccessToken')
97
+ @access_token.stub(:token)
98
+ @access_token.stub(:expires?)
99
+ @access_token.stub(:expires_at)
100
+ @access_token.stub(:refresh_token)
101
+ subject.stub(:access_token) { @access_token }
102
+ end
103
+
104
+ it 'returns a Hash' do
105
+ subject.credentials.should be_a(Hash)
106
+ end
107
+
108
+ it 'returns the token' do
109
+ @access_token.stub(:token) { '123' }
110
+ subject.credentials['token'].should eq('123')
111
+ end
112
+
113
+ it 'returns the expiry status' do
114
+ @access_token.stub(:expires?) { true }
115
+ subject.credentials['expires'].should eq(true)
116
+
117
+ @access_token.stub(:expires?) { false }
118
+ subject.credentials['expires'].should eq(false)
119
+ end
120
+
121
+ end
122
+
123
+ describe '#valid_site?' do
124
+ it 'returns true if the site contains .spiffystores.com' do
125
+ @options = {:client_options => {:site => 'http://foo.spiffystores.com/'}}
126
+ subject.valid_site?.should eq(true)
127
+ end
128
+
129
+ it 'returns false if the site does not contain .spiffystores.com' do
130
+ @options = {:client_options => {:site => 'http://foo.example.com/'}}
131
+ subject.valid_site?.should eq(false)
132
+ end
133
+
134
+ it 'uses configurable option for spiffy_stores_domain' do
135
+ @options = {:client_options => {:site => 'http://foo.example.com/'}, :spiffy_stores_domain => 'example.com'}
136
+ subject.valid_site?.should eq(true)
137
+ end
138
+
139
+ it 'allows custom port for spiffy_stores_domain' do
140
+ @options = {:client_options => {:site => 'http://foo.example.com:3456/'}, :spiffy_stores_domain => 'example.com:3456'}
141
+ subject.valid_site?.should eq(true)
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,366 @@
1
+ require_relative 'test_helper'
2
+
3
+ class IntegrationTest < Minitest::Test
4
+ def setup
5
+ build_app(scope: OmniAuth::Strategies::Spiffy::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('storedemo.spiffystores.com')
15
+ assert_equal 302, response.status
16
+ assert_match %r{\A#{Regexp.quote(spiffy_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/spiffy/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('storedemo.spiffystores.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['store']}"
35
+ }
36
+
37
+ response = authorize('storedemo.spiffystores.com')
38
+ assert_match %r{\A#{Regexp.quote(spiffy_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 .spiffystores.com
46
+ 'http://storedemo.spiffystores.com', # shop contains protocol
47
+ 'storedemo.spiffystores.com/path', # shop contains path
48
+ 'user@storedemo.spiffystores.com', # shop contains user
49
+ 'storedemo.spiffystores.com:22', # shop contains port
50
+ ].each do |shop, valid|
51
+ response = authorize(shop)
52
+ assert_auth_failure(response, 'invalid_site')
53
+
54
+ response = callback(sign_params(store: shop, code: code))
55
+ assert_auth_failure(response, 'invalid_site')
56
+ end
57
+ end
58
+
59
+ def test_callback
60
+ access_token = SecureRandom.hex(16)
61
+ code = SecureRandom.hex(16)
62
+ expect_access_token_request(access_token, OmniAuth::Strategies::Spiffy::DEFAULT_SCOPE)
63
+
64
+ response = callback(sign_params(store: 'storedemo.spiffystores.com', code: code, state: opts["rack.session"]["omniauth.state"]))
65
+
66
+ assert_callback_success(response, access_token, code)
67
+ end
68
+
69
+ def test_callback_with_legacy_signature
70
+ build_app scope: OmniAuth::Strategies::Spiffy::DEFAULT_SCOPE
71
+ access_token = SecureRandom.hex(16)
72
+ code = SecureRandom.hex(16)
73
+ expect_access_token_request(access_token, OmniAuth::Strategies::Spiffy::DEFAULT_SCOPE)
74
+
75
+ response = callback(sign_params(store: 'storedemo.spiffystores.com', code: code, state: opts["rack.session"]["omniauth.state"]).merge(signature: 'ignored'))
76
+
77
+ assert_callback_success(response, access_token, code)
78
+ end
79
+
80
+ def test_callback_custom_params
81
+ access_token = SecureRandom.hex(16)
82
+ code = SecureRandom.hex(16)
83
+ expect_access_token_request(access_token, OmniAuth::Strategies::Spiffy::DEFAULT_SCOPE)
84
+
85
+ now = Time.now.to_i
86
+ params = { store: 'storedemo.spiffystores.com', code: code, timestamp: now, next: '/products?page=2&q=red%20shirt', state: opts["rack.session"]["omniauth.state"] }
87
+ encoded_params = OmniAuth::Strategies::Spiffy.encoded_params_for_signature(params)
88
+ params[:hmac] = OmniAuth::Strategies::Spiffy.hmac_sign(encoded_params, @secret)
89
+
90
+ response = callback(params)
91
+
92
+ assert_callback_success(response, access_token, code)
93
+ end
94
+
95
+ def test_callback_with_spaces_in_scope
96
+ build_app scope: 'write_products, read_orders'
97
+ access_token = SecureRandom.hex(16)
98
+ code = SecureRandom.hex(16)
99
+ expect_access_token_request(access_token, 'read_orders,write_products')
100
+
101
+ response = callback(sign_params(store: 'storedemo.spiffystores.com', code: code, state: opts["rack.session"]["omniauth.state"]))
102
+
103
+ assert_callback_success(response, access_token, code)
104
+ end
105
+
106
+ def test_callback_rejects_invalid_hmac
107
+ @secret = 'wrong_secret'
108
+ response = callback(sign_params(store: 'storedemo.spiffystores.com', code: SecureRandom.hex(16)))
109
+
110
+ assert_auth_failure(response, 'invalid_signature')
111
+ end
112
+
113
+ def test_callback_rejects_old_timestamps
114
+ expired_timestamp = Time.now.to_i - OmniAuth::Strategies::Spiffy::CODE_EXPIRES_AFTER - 1
115
+ response = callback(sign_params(store: 'storedemo.spiffystores.com', code: SecureRandom.hex(16), timestamp: expired_timestamp))
116
+
117
+ assert_auth_failure(response, 'invalid_signature')
118
+ end
119
+
120
+ def test_callback_rejects_missing_hmac
121
+ code = SecureRandom.hex(16)
122
+
123
+ response = callback(store: 'storedemo.spiffystores.com', code: code, timestamp: Time.now.to_i)
124
+
125
+ assert_auth_failure(response, 'invalid_signature')
126
+ end
127
+
128
+ def test_callback_rejects_body_params
129
+ code = SecureRandom.hex(16)
130
+ params = sign_params(store: 'storedemo.spiffystores.com', code: code)
131
+ body = Rack::Utils.build_nested_query(unsigned: 'value')
132
+
133
+ response = request.get("https://app.example.com/auth/spiffy/callback?#{Rack::Utils.build_query(params)}",
134
+ input: body,
135
+ "CONTENT_TYPE" => 'application/x-www-form-urlencoded')
136
+
137
+ assert_auth_failure(response, 'invalid_signature')
138
+ end
139
+
140
+ def test_provider_options
141
+ build_app scope: 'read_products,read_orders,write_content',
142
+ callback_path: '/admin/auth/legacy/callback',
143
+ spiffy_stores_domain: 'spiffystores.dev:3000',
144
+ setup: lambda { |env|
145
+ shop = Rack::Request.new(env).GET['store']
146
+ shop += ".spiffystores.dev:3000" unless shop.include?(".")
147
+ env['omniauth.strategy'].options[:client_options][:site] = "https://#{shop}"
148
+ }
149
+
150
+ response = authorize('storedemo')
151
+ assert_equal 302, response.status
152
+ assert_match %r{\A#{Regexp.quote("https://storedemo.spiffystores.dev:3000/admin/oauth/authorize?")}}, response.location
153
+ redirect_params = Rack::Utils.parse_query(URI(response.location).query)
154
+ assert_equal 'read_products,read_orders,write_content', redirect_params['scope']
155
+ assert_equal 'https://app.example.com/admin/auth/legacy/callback', redirect_params['redirect_uri']
156
+ end
157
+
158
+ def test_unnecessary_read_scopes_are_removed
159
+ build_app scope: 'read_content,read_products,write_products',
160
+ callback_path: '/admin/auth/legacy/callback',
161
+ spiffy_stores_domain: 'spiffystores.dev:3000',
162
+ setup: lambda { |env|
163
+ shop = Rack::Request.new(env).GET['store']
164
+ shop += ".spiffystores.dev:3000" unless shop.include?(".")
165
+ env['omniauth.strategy'].options[:client_options][:site] = "https://#{shop}"
166
+ }
167
+
168
+ response = authorize('storedemo')
169
+ assert_equal 302, response.status
170
+ redirect_params = Rack::Utils.parse_query(URI(response.location).query)
171
+ assert_equal 'read_content,write_products', redirect_params['scope']
172
+ end
173
+
174
+ def test_callback_with_invalid_state_fails
175
+ access_token = SecureRandom.hex(16)
176
+ code = SecureRandom.hex(16)
177
+ expect_access_token_request(access_token, OmniAuth::Strategies::Spiffy::DEFAULT_SCOPE)
178
+
179
+ response = callback(sign_params(store: 'storedemo.spiffystores.com', code: code, state: 'invalid'))
180
+
181
+ assert_equal 302, response.status
182
+ assert_equal '/auth/failure?message=csrf_detected&strategy=spiffy', response.location
183
+ end
184
+
185
+ def test_callback_with_mismatching_scope_fails
186
+ access_token = SecureRandom.hex(16)
187
+ code = SecureRandom.hex(16)
188
+ expect_access_token_request(access_token, 'some_invalid_scope', nil)
189
+
190
+ response = callback(sign_params(store: 'storedemo.spiffystores.com', code: code, state: opts["rack.session"]["omniauth.state"]))
191
+
192
+ assert_equal 302, response.status
193
+ assert_equal '/auth/failure?message=invalid_scope&strategy=spiffy', response.location
194
+ end
195
+
196
+ def test_callback_with_no_scope_fails
197
+ access_token = SecureRandom.hex(16)
198
+ code = SecureRandom.hex(16)
199
+ expect_access_token_request(access_token, nil)
200
+
201
+ response = callback(sign_params(store: 'storedemo.spiffystores.com', code: code, state: opts["rack.session"]["omniauth.state"]))
202
+
203
+ assert_equal 302, response.status
204
+ assert_equal '/auth/failure?message=invalid_scope&strategy=spiffy', response.location
205
+ end
206
+
207
+ def test_callback_with_missing_access_scope_fails
208
+ build_app scope: 'first_scope,second_scope'
209
+
210
+ access_token = SecureRandom.hex(16)
211
+ code = SecureRandom.hex(16)
212
+ expect_access_token_request(access_token, 'first_scope')
213
+
214
+ response = callback(sign_params(store: 'storedemo.spiffystores.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=spiffy', response.location
218
+ end
219
+
220
+ def test_callback_with_extra_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, 'second_scope,first_scope,third_scope')
226
+
227
+ response = callback(sign_params(store: 'storedemo.spiffystores.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=spiffy', response.location
231
+ end
232
+
233
+ def test_callback_with_scopes_out_of_order_works
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')
239
+
240
+ response = callback(sign_params(store: 'storedemo.spiffystores.com', code: code, state: opts["rack.session"]["omniauth.state"]))
241
+
242
+ assert_callback_success(response, access_token, code)
243
+ end
244
+
245
+ def test_callback_with_extra_comma_works
246
+ build_app scope: 'read_content,,write_products,'
247
+
248
+ access_token = SecureRandom.hex(16)
249
+ code = SecureRandom.hex(16)
250
+ expect_access_token_request(access_token, 'read_content,write_products')
251
+
252
+ response = callback(sign_params(store: 'storedemo.spiffystores.com', code: code, state: opts["rack.session"]["omniauth.state"]))
253
+
254
+ assert_callback_success(response, access_token, code)
255
+ end
256
+
257
+ def test_callback_when_per_user_permissions_are_present_but_not_requested
258
+ build_app(scope: 'scope', per_user_permissions: false)
259
+
260
+ access_token = SecureRandom.hex(16)
261
+ code = SecureRandom.hex(16)
262
+ expect_access_token_request(access_token, 'scope', { id: 1, email: 'bob@bobsen.com'})
263
+
264
+ response = callback(sign_params(store: 'storedemo.spiffystores.com', code: code, state: opts["rack.session"]["omniauth.state"]))
265
+
266
+ assert_equal 302, response.status
267
+ assert_equal '/auth/failure?message=invalid_permissions&strategy=spiffy', response.location
268
+ end
269
+
270
+ def test_callback_when_per_user_permissions_are_not_present_but_requested
271
+ build_app(scope: 'scope', per_user_permissions: true)
272
+
273
+ access_token = SecureRandom.hex(16)
274
+ code = SecureRandom.hex(16)
275
+ expect_access_token_request(access_token, 'scope', nil)
276
+
277
+ response = callback(sign_params(store: 'storedemo.spiffystores.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=spiffy', response.location
281
+ end
282
+
283
+ def test_callback_works_when_per_user_permissions_are_present_and_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', { id: 1, email: 'bob@bobsen.com'})
289
+
290
+ response = callback(sign_params(store: 'storedemo.spiffystores.com', code: code, state: opts["rack.session"]["omniauth.state"]))
291
+
292
+ assert_equal 200, response.status
293
+ end
294
+
295
+ private
296
+
297
+ def sign_params(params)
298
+ params = params.dup
299
+
300
+ params[:timestamp] ||= Time.now.to_i
301
+
302
+ encoded_params = OmniAuth::Strategies::Spiffy.encoded_params_for_signature(params)
303
+ params[:hmac] = OmniAuth::Strategies::Spiffy.hmac_sign(encoded_params, @secret)
304
+ params
305
+ end
306
+
307
+ def expect_access_token_request(access_token, scope, associated_user=nil)
308
+ FakeWeb.register_uri(:post, "https://storedemo.spiffystores.com/admin/oauth/token",
309
+ body: JSON.dump(access_token: access_token, scope: scope, associated_user: associated_user),
310
+ content_type: 'application/json')
311
+ end
312
+
313
+ def assert_callback_success(response, access_token, code)
314
+ token_request_params = Rack::Utils.parse_query(FakeWeb.last_request.body)
315
+ assert_equal token_request_params['client_id'], '123'
316
+ assert_equal token_request_params['client_secret'], @secret
317
+ assert_equal token_request_params['code'], code
318
+
319
+ assert_equal 'storedemo.spiffystores.com', @omniauth_result.uid
320
+ assert_equal access_token, @omniauth_result.credentials.token
321
+ assert_equal false, @omniauth_result.credentials.expires
322
+
323
+ assert_equal 200, response.status
324
+ assert_equal "OK", response.body
325
+ end
326
+
327
+ def assert_auth_failure(response, reason)
328
+ assert_nil FakeWeb.last_request
329
+ assert_equal 302, response.status
330
+ assert_match %r{\A#{Regexp.quote("/auth/failure?message=#{reason}")}}, response.location
331
+ end
332
+
333
+ def build_app(options={})
334
+ app = proc { |env|
335
+ @omniauth_result = env['omniauth.auth']
336
+ [200, {Rack::CONTENT_TYPE => "text/plain"}, "OK"]
337
+ }
338
+
339
+ opts["rack.session"]["omniauth.state"] = SecureRandom.hex(32)
340
+ app = OmniAuth::Builder.new(app) do
341
+ provider :spiffy, '123', '53cr3tz', options
342
+ end
343
+ @secret = '53cr3tz'
344
+ @app = Rack::Session::Cookie.new(app, secret: SecureRandom.hex(64))
345
+ end
346
+
347
+ def authorize(shop)
348
+ request.get("https://app.example.com/auth/spiffy?store=#{CGI.escape(shop)}", opts)
349
+ end
350
+
351
+ def callback(params)
352
+ request.get("https://app.example.com/auth/spiffy/callback?#{Rack::Utils.build_query(params)}", opts)
353
+ end
354
+
355
+ def opts
356
+ @opts ||= { "rack.session" => {} }
357
+ end
358
+
359
+ def request
360
+ Rack::MockRequest.new(@app)
361
+ end
362
+
363
+ def spiffy_authorize_url
364
+ "https://storedemo.spiffystores.com/admin/oauth/authorize?"
365
+ end
366
+ end
@@ -0,0 +1,10 @@
1
+ $: << File.expand_path("../../lib", __FILE__)
2
+ require 'bundler/setup'
3
+ require 'omniauth-spiffy-oauth2'
4
+
5
+ require 'minitest/autorun'
6
+ require 'fakeweb'
7
+ require 'json'
8
+
9
+ OmniAuth.config.logger = Logger.new(nil)
10
+ FakeWeb.allow_net_connect = false
metadata ADDED
@@ -0,0 +1,119 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: omniauth-spiffy-oauth2
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.2.1
5
+ platform: ruby
6
+ authors:
7
+ - Spiffy Stores
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-06-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: omniauth-oauth2
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 1.5.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 1.5.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: minitest
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '5.6'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '5.6'
41
+ - !ruby/object:Gem::Dependency
42
+ name: fakeweb
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.3'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.3'
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
+ - brian@spiffy.com.au
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - ".gitignore"
77
+ - ".travis.yml"
78
+ - Gemfile
79
+ - README.md
80
+ - Rakefile
81
+ - example/Gemfile
82
+ - example/config.ru
83
+ - lib/omniauth-spiffy-oauth2.rb
84
+ - lib/omniauth/spiffy.rb
85
+ - lib/omniauth/spiffy/version.rb
86
+ - lib/omniauth/strategies/spiffy.rb
87
+ - omniauth-spiffy-oauth2.gemspec
88
+ - shipit.rubygems.yml
89
+ - spec/omniauth/strategies/spiffy_spec.rb
90
+ - test/integration_test.rb
91
+ - test/test_helper.rb
92
+ homepage: https://github.com/SpiffyStores/omniauth-spiffy-oauth2
93
+ licenses:
94
+ - MIT
95
+ metadata: {}
96
+ post_install_message:
97
+ rdoc_options: []
98
+ require_paths:
99
+ - lib
100
+ required_ruby_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: 2.1.9
105
+ required_rubygems_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ requirements: []
111
+ rubyforge_project:
112
+ rubygems_version: 2.7.7
113
+ signing_key:
114
+ specification_version: 4
115
+ summary: Spiffy Stores strategy for OmniAuth
116
+ test_files:
117
+ - spec/omniauth/strategies/spiffy_spec.rb
118
+ - test/integration_test.rb
119
+ - test/test_helper.rb