omniauth-shopify-app 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 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'