omniauth-shopify-app 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e0adc6d98970d118184067f144d6cc6d07a9ec346e6add26b1b3bf97bf2d6970
4
+ data.tar.gz: db9f41aae849ffd1a55559a85258973ab4af59554a20f1a4fda7f88c26b0bf91
5
+ SHA512:
6
+ metadata.gz: cb171fd3fe0eeb681215d60302c8f1d11af051b08337871a3eacc17713e69374c71196bed98e8b643aaff1b9700ad1a648b609502d4bcbd70c6817e8064055fd
7
+ data.tar.gz: '080bfe5fc359bafd7633308930dbd78074628af138748faa01c7f79a75f0188a2c944cc072533ee965de64f20f72805fdf2b91caded051fdeb1db064f4249d2a'
@@ -0,0 +1,25 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+
6
+ jobs:
7
+ build:
8
+ runs-on: ubuntu-latest
9
+ name: Ruby ${{ matrix.version }}
10
+ strategy:
11
+ matrix:
12
+ version: [2.7, 3.0, 3.1]
13
+
14
+ steps:
15
+ - uses: actions/checkout@v2
16
+ - name: Set up Ruby ${{ matrix.version }}
17
+ uses: ruby/setup-ruby@v1
18
+ with:
19
+ ruby-version: ${{ matrix.version }}
20
+ bundler-cache: true
21
+ - name: Install dependencies
22
+ run: bundle
23
+ - name: Run Tests
24
+ run: bundle exec rake
25
+
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ pkg/*
2
+ Gemfile.lock
3
+ .byebug_history
4
+ *.gem
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ group :development, :test do
6
+ gem 'fakeweb', git: 'https://github.com/chrisk/fakeweb.git'
7
+ gem 'byebug'
8
+ end
data/README.md ADDED
@@ -0,0 +1,107 @@
1
+ # OmniAuth Shopify
2
+
3
+ Shopify OAuth2 Strategy for OmniAuth 1.0.
4
+
5
+ ## Installing
6
+
7
+ Add to your `Gemfile`:
8
+
9
+ ```ruby
10
+ gem 'omniauth-shopify-app'
11
+ ```
12
+
13
+ Then `bundle install`.
14
+
15
+ ## Usage
16
+
17
+ `OmniAuth::Strategies::Shopify` is simply a Rack middleware. Read [the OmniAuth 1.0 docs](https://github.com/intridea/omniauth) for detailed instructions.
18
+
19
+ Here's a quick example, adding the middleware to a Rails app in `config/initializers/omniauth.rb`:
20
+
21
+ ```ruby
22
+ Rails.application.config.middleware.use OmniAuth::Builder do
23
+ provider :shopify, ENV['SHOPIFY_API_KEY'], ENV['SHOPIFY_SHARED_SECRET']
24
+ end
25
+ ```
26
+
27
+ Authenticate the user by having them visit /auth/shopify with a `shop` query parameter of their shop's myshopify.com domain. For example, the following form could be used
28
+
29
+ ```html
30
+ <form action="/auth/shopify" method="get">
31
+ <label for="shop">Enter your store's URL:</label>
32
+ <input type="text" name="shop" placeholder="your-shop-url.myshopify.com">
33
+ <button type="submit">Log In</button>
34
+ </form>
35
+ ```
36
+
37
+ Or without form `/auth/shopify?shop=your-shop-url.myshopify.com`
38
+ Alternatively you can put shop parameter to session as [Shopify App](https://github.com/Shopify/shopify_app) do
39
+
40
+ ```ruby
41
+ session['shopify.omniauth_params'] = { shop: params[:shop] }
42
+ ```
43
+
44
+ And finally it's possible to use your own query parameter by overriding default setup method. For example, like below:
45
+
46
+ ```ruby
47
+ Rails.application.config.middleware.use OmniAuth::Builder do
48
+ provider :shopify,
49
+ ENV['SHOPIFY_API_KEY'],
50
+ ENV['SHOPIFY_SHARED_SECRET'],
51
+ option :setup, proc { |env|
52
+ strategy = env['omniauth.strategy']
53
+
54
+
55
+
56
+ site = if strategy.request.params['site']
57
+ "https://#{strategy.request.params['site']}"
58
+ else
59
+ ''
60
+ end
61
+
62
+ env['omniauth.strategy'].options[:client_options][:site] = site
63
+ }
64
+ ```
65
+
66
+ ## Configuring
67
+
68
+ ### Scope
69
+
70
+ You can configure the scope, which you pass in to the `provider` method via a `Hash`:
71
+
72
+ * `scope`: A comma-separated list of permissions you want to request from the user. See [the Shopify API docs](http://docs.shopify.com/api/tutorials/oauth) for a full list of available permissions.
73
+
74
+ For example, to request `read_products`, `read_orders` and `write_content` permissions and display the authentication page:
75
+
76
+ ```ruby
77
+ Rails.application.config.middleware.use OmniAuth::Builder do
78
+ provider :shopify, ENV['SHOPIFY_API_KEY'], ENV['SHOPIFY_SHARED_SECRET'], :scope => 'read_products,read_orders,write_content'
79
+ end
80
+ ```
81
+
82
+ ### Online Access
83
+
84
+ Shopify offers two different types of access tokens: [online access and offline access](https://help.shopify.com/api/getting-started/authentication/oauth/api-access-modes). You can configure for online-access by passing the `per_user_permissions` option:
85
+
86
+ ```
87
+ Rails.application.config.middleware.use OmniAuth::Builder do
88
+ provider :shopify, ENV['SHOPIFY_API_KEY'],
89
+ ENV['SHOPIFY_SHARED_SECRET'],
90
+ :scope => 'read_orders',
91
+ :per_user_permissions => true
92
+ end
93
+ ```
94
+
95
+ ## Authentication Hash
96
+
97
+ Here's an example *Authentication Hash* available in `request.env['omniauth.auth']`:
98
+
99
+ ```ruby
100
+ {
101
+ :provider => 'shopify',
102
+ :uid => 'example.myshopify.com',
103
+ :credentials => {
104
+ :token => 'afasd923kjh0934kf', # OAuth 2.0 access_token, which you store and use to authenticate API requests
105
+ }
106
+ }
107
+ ```
data/Rakefile ADDED
@@ -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
data/example/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org/'
2
+
3
+ gem 'rack', '~> 1.6'
4
+
5
+ gem 'sinatra', '~> 1.4'
6
+ gem 'omniauth-shopify-app', :path => '../'
data/example/config.ru ADDED
@@ -0,0 +1,68 @@
1
+ require 'bundler/setup'
2
+ require 'sinatra/base'
3
+ require 'active_support/core_ext/hash'
4
+ require 'omniauth-shopify-app'
5
+
6
+ SCOPE = 'read_products,read_orders,read_customers,write_shipping'
7
+ SHOPIFY_API_KEY = ENV['SHOPIFY_API_KEY']
8
+ SHOPIFY_SHARED_SECRET = ENV['SHOPIFY_SHARED_SECRET']
9
+
10
+ unless SHOPIFY_API_KEY && SHOPIFY_SHARED_SECRET
11
+ abort("SHOPIFY_API_KEY and SHOPIFY_SHARED_SECRET environment variables must be set")
12
+ end
13
+
14
+ class App < Sinatra::Base
15
+ get '/' do
16
+ <<-HTML
17
+ <html>
18
+ <head>
19
+ <title>Shopify Oauth2</title>
20
+ </head>
21
+ <body>
22
+ <form action="/auth/shopify" method="get">
23
+ <label for="shop">Enter your store's URL:</label>
24
+ <input type="text" name="shop" placeholder="your-shop-url.myshopify.com">
25
+ <button type="submit">Log In</button>
26
+ </form>
27
+ </body>
28
+ </html>
29
+ HTML
30
+ end
31
+
32
+ get '/auth/:provider/callback' do
33
+ <<-HTML
34
+ <html>
35
+ <head>
36
+ <title>Shopify Oauth2</title>
37
+ </head>
38
+ <body>
39
+ <h3>Authorized</h3>
40
+ <p>Shop: #{request.env['omniauth.auth'].uid}</p>
41
+ <p>Token: #{request.env['omniauth.auth']['credentials']['token']}</p>
42
+ </body>
43
+ </html>
44
+ HTML
45
+ end
46
+
47
+ get '/auth/failure' do
48
+ <<-HTML
49
+ <html>
50
+ <head>
51
+ <title>Shopify Oauth2</title>
52
+ </head>
53
+ <body>
54
+ <h3>Failed Authorization</h3>
55
+ <p>Message: #{params[:message]}</p>
56
+ </body>
57
+ </html>
58
+ HTML
59
+ end
60
+ end
61
+
62
+ use Rack::Session::Cookie, secret: SecureRandom.hex(64)
63
+
64
+ use OmniAuth::Builder do
65
+ provider :shopify, SHOPIFY_API_KEY, SHOPIFY_SHARED_SECRET, :scope => SCOPE
66
+ end
67
+
68
+ run App.new
@@ -0,0 +1,121 @@
1
+ require 'openssl'
2
+
3
+ module OmniAuth
4
+ module Shopify
5
+ # Encrypts messages with authentication
6
+ #
7
+ # The use of authentication is essential to avoid Chosen Ciphertext
8
+ # Attacks. By using this in an encrypt then MAC form, we avoid some
9
+ # attacks such as e.g. being used as a CBC padding oracle to decrypt
10
+ # the ciphertext.
11
+ class Encryptor
12
+ # Create the encryptor
13
+ #
14
+ # Pass in the secret, which should be at least 32-bytes worth of
15
+ # entropy, e.g. a string generated by `SecureRandom.hex(32)`.
16
+ # This also allows specification of the algorithm for the cipher
17
+ # and MAC. But don't change that unless you're very sure.
18
+ def initialize(secret, cipher = 'aes-256-cbc', hmac = 'SHA256')
19
+ @cipher = cipher
20
+ @hmac = hmac
21
+
22
+ # use the HMAC to derive two independent keys for the encryption and
23
+ # authentication of ciphertexts It is bad practice to use the same key
24
+ # for encryption and authentication. This also allows us to use all
25
+ # of the entropy in a long key (e.g. 64 hex bytes) when straight
26
+ # assignement would could result in assigning a key with a much
27
+ # reduced key space. Also, the personalisation strings further help
28
+ # reduce the possibility of key reuse by ensuring it should be unique
29
+ # to this gem, even with shared secrets.
30
+ @encryption_key = hmac("EncryptedCookie Encryption", secret)
31
+ @authentication_key = hmac("EncryptedCookie Authentication", secret)
32
+ end
33
+
34
+ # Encrypts message
35
+ #
36
+ # Returns the base64 encoded ciphertext plus IV. In addtion, the
37
+ # message is prepended with a MAC code to prevent chosen ciphertext
38
+ # attacks.
39
+ def encrypt(message)
40
+ # encrypt the message
41
+ encrypted = encrypt_message(message)
42
+
43
+ [authenticate_message(encrypted) + encrypted].pack('m0')
44
+ end
45
+
46
+ # decrypts base64 encoded ciphertext
47
+ #
48
+ # First, it checks the message tag and returns nil if that fails to verify.
49
+ # Otherwise, the data is passed on to the AES function for decryption.
50
+ def decrypt(ciphertext)
51
+ ciphertext = ciphertext.unpack('m').first
52
+ tag = ciphertext[0, hmac_length]
53
+ ciphertext = ciphertext[hmac_length..-1]
54
+
55
+ # make sure we actually had enough data for the tag too.
56
+ if tag && ciphertext && verify_message(tag, ciphertext)
57
+ decrypt_ciphertext(ciphertext)
58
+ else
59
+ nil
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ # HMAC digest of the message using the given secret
66
+ def hmac(secret, message)
67
+ OpenSSL::HMAC.digest(@hmac, secret, message)
68
+ end
69
+
70
+ def hmac_length
71
+ OpenSSL::Digest.new(@hmac).size
72
+ end
73
+
74
+ # returns the message authentication tag
75
+ #
76
+ # This is computed as HMAC(authentication_key, message)
77
+ def authenticate_message(message)
78
+ hmac(@authentication_key, message)
79
+ end
80
+
81
+ # verifies the message
82
+ #
83
+ # This does its best to be constant time, by use of the rack secure compare
84
+ # function.
85
+ def verify_message(tag, message)
86
+ own_tag = authenticate_message(message)
87
+ Rack::Utils.secure_compare(tag, own_tag)
88
+ end
89
+
90
+ # Encrypt
91
+ #
92
+ # Encrypts the given message with a random IV, then returns the ciphertext
93
+ # with the IV prepended.
94
+ def encrypt_message(message)
95
+ aes = OpenSSL::Cipher.new(@cipher).encrypt
96
+ aes.key = @encryption_key
97
+ iv = aes.random_iv
98
+ aes.iv = iv
99
+ iv + (aes.update(message) << aes.final)
100
+ end
101
+
102
+ # Decrypt
103
+ #
104
+ # Pulls the IV off the front of the message and decrypts. Catches
105
+ # OpenSSL errors and returns nil. But this should never happen, as the
106
+ # verify method should catch all corrupted ciphertexts.
107
+ def decrypt_ciphertext(ciphertext)
108
+ aes = OpenSSL::Cipher.new(@cipher).decrypt
109
+ aes.key = @encryption_key
110
+ iv = ciphertext[0, aes.iv_len]
111
+ aes.iv = iv
112
+ crypted_text = ciphertext[aes.iv_len..-1]
113
+ return nil if crypted_text.nil? || iv.nil?
114
+ aes.update(crypted_text) << aes.final
115
+ rescue
116
+ nil
117
+ end
118
+
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,81 @@
1
+ require 'rack'
2
+ require_relative './encryptor'
3
+
4
+ # Copy and modify from https://github.com/cvonkleist/encrypted_cookie
5
+
6
+ module OmniAuth
7
+ module Shopify
8
+ class OAuthSession
9
+ EXPIRES = '_encrypted_cookie_expires_'
10
+ KEY = 'shopify_oauth_session'
11
+
12
+ def initialize(app, options={})
13
+ @app = app
14
+ @key = options[:key] || KEY
15
+ @secret = options[:secret]
16
+ fail "Error! A secret is required to use encrypted cookies. Do something like this:\n\nuse OmniAuth::Shopify::OauthSession, :secret => YOUR_VERY_LONG_VERY_RANDOM_SECRET_KEY_HERE" unless @secret
17
+ @default_options = {:domain => nil,
18
+ :path => "/",
19
+ :time_to_live => 1800,
20
+ :expire_after => nil}.merge(options)
21
+ @encryptor = Encryptor.new(@secret)
22
+ end
23
+
24
+ def call(env)
25
+ load_session(env)
26
+ status, headers, body = @app.call(env)
27
+ commit_session(env, status, headers, body)
28
+ end
29
+
30
+ private
31
+
32
+ def remove_expiration(session_data)
33
+ expires = session_data.delete(EXPIRES)
34
+ if expires and expires < Time.now
35
+ session_data.clear
36
+ end
37
+ end
38
+
39
+ def load_session(env)
40
+ request = Rack::Request.new(env)
41
+ env["rack.#{@key}.options"] = @default_options.dup
42
+
43
+ session_data = request.cookies[@key]
44
+ session_data = @encryptor.decrypt(session_data)
45
+ session_data = Marshal.load(session_data)
46
+ remove_expiration(session_data)
47
+
48
+ env["rack.#{@key}"] = session_data
49
+ rescue
50
+ env["rack.#{@key}"] = Hash.new
51
+ end
52
+
53
+ def add_expiration(session_data, options)
54
+ if options[:time_to_live] && !session_data.key?(EXPIRES)
55
+ expires = Time.now + options[:time_to_live]
56
+ session_data.merge!({EXPIRES => expires})
57
+ end
58
+ end
59
+
60
+ def commit_session(env, status, headers, body)
61
+ options = env["rack.#{@key}.options"]
62
+
63
+ session_data = env["rack.#{@key}"]
64
+ add_expiration(session_data, options)
65
+ session_data = Marshal.dump(session_data)
66
+ session_data = @encryptor.encrypt(session_data)
67
+
68
+ if session_data.size > (4096 - @key.size)
69
+ env["rack.errors"].puts("Warning! ShopifyOAuthSession data size exceeds 4K. Content dropped.")
70
+ else
71
+ cookie = Hash.new
72
+ cookie[:value] = session_data
73
+ cookie[:expires] = Time.now + options[:expire_after] unless options[:expire_after].nil?
74
+ Rack::Utils.set_cookie_header!(headers, @key, cookie.merge(options))
75
+ end
76
+
77
+ [status, headers, body]
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,5 @@
1
+ module OmniAuth
2
+ module Shopify
3
+ VERSION = "1.0.0"
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ require 'omniauth/shopify/version'
2
+ require 'omniauth/shopify/oauth_session'
3
+ require 'omniauth/strategies/shopify'