omniauth-shopline 0.1.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: 01e751e6d88dcef0f68c96602a5d3a335602d9deb45a946d5c3c077492bac924
4
+ data.tar.gz: f3fad501ab98759d9a4721b715488ea48ba50a54992848df9234eaef3ce55b2a
5
+ SHA512:
6
+ metadata.gz: ff0a41880509b2cce52b843a03017c486ddb167b28889b171a39e406b049b731a44d8652b88bfe78fb45e2b8494173fa8e485a45582063ec737c2a36fd280755
7
+ data.tar.gz: c87fa791adf2c9e92299256af917a56736a0084acfb191ab4be080a37416f632cc6f3dc1702f6ad353903251426c5677371cfeeb0a8fe6e245b2f58c0b364894
data/CHANGELOG.md ADDED
@@ -0,0 +1,78 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file. Each version heading
4
+ links to its [GitHub release](https://github.com/dropstream/omniauth-shopline/releases),
5
+ where the notes are generated from the merged pull requests.
6
+
7
+ ## [0.1.0] - 2026-08-27
8
+
9
+ First release published to RubyGems.org. Everything below is relative to the state of
10
+ `master` before it, which is how the gem was installed until now (`git:` in a Gemfile).
11
+
12
+ ### Changed
13
+
14
+ - Runtime dependencies are declared as `omniauth >= 2.0, < 4` and
15
+ `omniauth-oauth2 >= 1.8, < 3`. A plugin's ceiling becomes a ceiling in every
16
+ application that installs it, so this allows the next major of both gems rather than
17
+ blocking consumers on the day one ships. The floors are where OmniAuth 2 support
18
+ actually begins — `omniauth-oauth2` 1.8.0 is its first release to require
19
+ `omniauth ~> 2.0` — and CI runs the suite against those floors as well as against the
20
+ newest resolvable versions.
21
+ - Requires Ruby >= 3.2, tested on 3.2 through 4.0.
22
+ - The app key and secret are read from the `client_id`/`client_secret` positional
23
+ arguments, with `app_key`/`app_secret` accepted as named options. Previously the
24
+ strategy read `options.app_key`, which the positional arguments never set, so every
25
+ request went out with an empty `appKey`.
26
+ - `client_options[:site]` is only derived from `handle` when it still holds the
27
+ `{handle}` placeholder, so an explicitly configured site now survives.
28
+ - Removed the unused `token_params` default (`grant_type: authorization_code`). The
29
+ token endpoint takes only `{"code": ...}`, so the option never reached SHOPLINE.
30
+ - Development toolchain: Bundler 4.x, `rake ~> 13.0`, `rspec ~> 3.13`, `webmock ~> 3.25`.
31
+ - Added GitHub Actions for CI, a weekly `bundle-audit` run, and releases published to
32
+ RubyGems.org from the GitHub Releases UI via trusted publishing (OIDC, no stored API
33
+ key).
34
+
35
+ ### Added
36
+
37
+ - `LICENSE.txt` (MIT) is declared in the gemspec, along with `required_ruby_version` and
38
+ the usual `homepage`/`changelog`/`rubygems_mfa_required` metadata.
39
+ - CSRF protection for the callback. SHOPLINE has no `state` parameter, so the nonce
40
+ travels in `customField` — the documented pass-through — and is verified against the
41
+ session. Disable with `verify_state: false`.
42
+ - Verification of the `sign` SHOPLINE appends to the callback query, computed the way
43
+ [SHOPLINE's own SDK](https://github.com/shoplineos/shopline-sdk-go/blob/main/client/sign.go)
44
+ computes it. Disable with `verify_signature: false`.
45
+ - `scope` in the credentials hash, which the README documented but the code did not
46
+ provide.
47
+ - `handle` is exposed as a reader on the strategy.
48
+ - Integration specs covering the request phase, the token exchange and its signature,
49
+ state and signature verification, the auth hash, and the `callback_url` overrides.
50
+
51
+ ### Fixed
52
+
53
+ - The authorize URL put its query string in front of the `#` fragment
54
+ (`/admin/oauth-web/?appKey=...#/oauth/authorize`), because `OAuth2::Client#authorize_url`
55
+ cannot build a URL for a hash-routed page. SHOPLINE's admin page never read those
56
+ params. It is now built by hand as
57
+ `/admin/oauth-web/#/oauth/authorize?appKey=...`.
58
+ - The token request was signed with the sorted-parameter scheme SHOPLINE uses for GETs
59
+ (`appkey=..&timestamp=..`). A POST is signed over the request body concatenated with
60
+ the millisecond timestamp, and the signed string is now the exact body that is sent.
61
+ - Every callback failed with `csrf_detected`: the request phase never wrote
62
+ `omniauth.state`, and SHOPLINE does not echo a `state` parameter, so
63
+ `OmniAuth::Strategies::OAuth2#callback_phase` rejected the callback before the token
64
+ exchange.
65
+ - `callback_url` no longer doubles a mount prefix. The override added `script_name` to
66
+ `callback_path`, but OmniAuth 2 moved `script_name` into `callback_path` itself, so a
67
+ strategy mounted under a prefix (for example Devise's `/users`) would have sent
68
+ `redirectUri=http://host/users/users/auth/shopline/callback`.
69
+ - A token response without an access token used to `fail!` from inside
70
+ `build_access_token` and hand the resulting Rack triple back as the access token. It
71
+ now raises `OAuth2::Error`, which OmniAuth turns into an `invalid_credentials` failure.
72
+ - The callback nonce and signature are compared in constant time.
73
+ - The token's `scope` reaches `OAuth2::AccessToken` under a string key. It was passed
74
+ with a symbol key, which `access_token.params['scope']` never found, so
75
+ `extra['scope']` was always `nil`.
76
+ - Development scripts in `bin/` and the spec suite are no longer packaged into the gem.
77
+
78
+ [0.1.0]: https://github.com/dropstream/omniauth-shopline/releases/tag/v0.1.0
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Dropstream
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,198 @@
1
+ # OmniAuth SHOPLINE
2
+
3
+ An OmniAuth strategy for authenticating with [SHOPLINE](https://www.shopline.com) using
4
+ its [OAuth 2.0 app authorization flow](https://developer.shopline.com/docs/apps/api-instructions-for-use/app-authorization/).
5
+
6
+ Requires Ruby >= 3.2 and OmniAuth 2.x.
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ ```ruby
13
+ gem 'omniauth-shopline'
14
+ ```
15
+
16
+ And then execute:
17
+
18
+ $ bundle
19
+
20
+ Or install it yourself as:
21
+
22
+ $ gem install omniauth-shopline
23
+
24
+ ## Usage
25
+
26
+ OmniAuth 2 only accepts `POST` for the request phase. In a Rails app, add
27
+ [`omniauth-rails_csrf_protection`](https://github.com/cookpad/omniauth-rails_csrf_protection)
28
+ to your Gemfile and link to the provider with `button_to` or `link_to ..., method: :post`:
29
+
30
+ ```ruby
31
+ gem 'omniauth-rails_csrf_protection'
32
+ ```
33
+
34
+ ```ruby
35
+ Rails.application.config.middleware.use OmniAuth::Builder do
36
+ provider :shopline,
37
+ 'app_key', 'app_secret',
38
+ handle: 'your-store',
39
+ scope: 'read_products,read_orders'
40
+ end
41
+ ```
42
+
43
+ `handle` is the store subdomain (`your-store` in `https://your-store.myshopline.com`) and
44
+ is required — it is what the authorize and token endpoints are addressed against. It is
45
+ read once, when the strategy is built, so serving several stores means one `provider`
46
+ entry per store.
47
+
48
+ The two positional arguments are SHOPLINE's app key and app secret. They may also be
49
+ passed by name, which takes precedence:
50
+
51
+ ```ruby
52
+ provider :shopline, handle: 'your-store', app_key: 'app_key', app_secret: 'app_secret'
53
+ ```
54
+
55
+ ## Auth hash
56
+
57
+ ```ruby
58
+ auth = request.env['omniauth.auth']
59
+
60
+ auth['provider'] # => "shopline"
61
+ auth['credentials']['token'] # => the access token, valid for ten hours
62
+ auth['credentials']['expires_at'] # => 1762800000 (seconds since the epoch)
63
+ auth['credentials']['expires'] # => true
64
+ auth['credentials']['scope'] # => "read_products,read_orders"
65
+ auth['extra']['handle'] # => "your-store"
66
+ auth['extra']['scope'] # => "read_products,read_orders"
67
+ ```
68
+
69
+ SHOPLINE's token response identifies a store, not a person, and the strategy does not set
70
+ `uid`. Key records on `extra['handle']`.
71
+
72
+ ## Configuring
73
+
74
+ ### The callback URL
75
+
76
+ The strategy overrides `callback_url` to drop the query string OmniAuth would otherwise
77
+ append, because SHOPLINE matches `redirectUri` against the app's registered callback URLs
78
+ exactly. Otherwise it is derived from the request, including the mount prefix — a strategy
79
+ mounted under Devise's `/users` produces
80
+ `https://app.example.com/users/auth/shopline/callback`.
81
+
82
+ Pass `redirect_uri` (or `callback_url`) to override it outright:
83
+
84
+ ```ruby
85
+ redirect_uri: 'https://app.example.com/users/auth/shopline/callback'
86
+ ```
87
+
88
+ ### Callback verification
89
+
90
+ SHOPLINE's authorize endpoint has no `state` parameter, so the CSRF nonce travels in
91
+ `customField`, the documented pass-through, and is checked against the session when the
92
+ callback comes back. SHOPLINE also signs the callback query string; the strategy verifies
93
+ that `sign` the way [SHOPLINE's own SDK](https://github.com/shoplineos/shopline-sdk-go/blob/main/client/sign.go)
94
+ does. Both checks are on by default and can be turned off individually:
95
+
96
+ ```ruby
97
+ verify_state: false, # skip the customField nonce check
98
+ verify_signature: false # skip the `sign` check
99
+ ```
100
+
101
+ `provider_ignores_state` is left at `true` on purpose: SHOPLINE never sends `state`, so
102
+ `OmniAuth::Strategies::OAuth2`'s own check can never pass. Use `verify_state` instead.
103
+
104
+ ### The store host
105
+
106
+ `client_options[:site]` defaults to `https://{handle}.myshopline.com` with the placeholder
107
+ filled in from `handle`. Setting it explicitly wins, which is useful for pointing the
108
+ strategy at a proxy or a test double:
109
+
110
+ ```ruby
111
+ client_options: { site: 'https://shopline.test' }
112
+ ```
113
+
114
+ ## How it differs from a plain OAuth 2 strategy
115
+
116
+ Three parts of SHOPLINE's flow are not standard OAuth 2, and the strategy handles each
117
+ itself rather than inheriting it:
118
+
119
+ 1. The authorize endpoint is a hash-routed admin page
120
+ (`/admin/oauth-web/#/oauth/authorize`), so its query string sits *after* the fragment
121
+ and cannot be built by `OAuth2::Client#authorize_url`.
122
+ 2. There is no `state` parameter (see above).
123
+ 3. `POST /admin/oauth/token/create` authenticates with `appkey`, `timestamp` and `sign`
124
+ headers rather than client credentials, takes `{"code": "..."}` as its whole body, and
125
+ answers with the token nested under `data`. SHOPLINE signs a POST over the request body
126
+ concatenated with the millisecond timestamp — not the sorted-parameter scheme it uses
127
+ for GETs.
128
+
129
+ ## Development
130
+
131
+ After checking out the repo, run `bin/setup` to install dependencies. Then run
132
+ `bundle exec rake spec` to run the tests. You can also run `bin/console` for an
133
+ interactive prompt.
134
+
135
+ To install this gem onto your local machine, run `bundle exec rake install`.
136
+
137
+ The gemspec allows a wide dependency range, so CI also runs the suite against the oldest
138
+ versions it permits. To reproduce that locally:
139
+
140
+ $ BUNDLE_GEMFILE=gemfiles/floor.gemfile bundle install
141
+ $ BUNDLE_GEMFILE=gemfiles/floor.gemfile bundle exec rake spec
142
+
143
+ Run `bundle exec bundle-audit check --update` to check the locked dependency tree against
144
+ the advisory database; CI runs the same check weekly.
145
+
146
+ ## Releasing
147
+
148
+ Releases are cut from the [GitHub Releases UI](https://github.com/dropstream/omniauth-shopline/releases/new);
149
+ GitHub Actions builds and publishes the gem to [rubygems.org](https://rubygems.org) via
150
+ [RubyGems trusted publishing](https://guides.rubygems.org/trusted-publishing/), so no API
151
+ key is stored in the repo and the gemspec can keep `rubygems_mfa_required`.
152
+
153
+ 1. Bump `VERSION` in `lib/omniauth-shopline/version.rb` and add a `CHANGELOG.md` section
154
+ for it, along with a link definition for the new heading at the bottom of that file.
155
+ Commit both on `master`.
156
+ 2. Draft a new release with the target set to `master` and the tag set to the version
157
+ prefixed with `v` — `v0.1.0`, not `0.1.0`. The prefix is stripped before comparing
158
+ against `version.rb`; the gem version itself carries no `v`.
159
+ 3. Click **Generate release notes** — the preferred method — then publish. The generated
160
+ notes list the merged pull requests; `CHANGELOG.md` carries the curated prose and
161
+ links back to each release.
162
+
163
+ Publishing creates the tag, which fires the `Release` workflow: it reruns the full test
164
+ matrix, refuses to continue if the tag and `version.rb` disagree, and pushes the gem.
165
+ Note that RubyGems versions are immutable — a bad release can be yanked, never replaced,
166
+ so the version bump has to be committed before the release is published.
167
+
168
+ ### One-time RubyGems setup
169
+
170
+ This gem has never been pushed, so the first release needs a **pending** trusted
171
+ publisher, which both claims the name `omniauth-shopline` and makes you an owner of it
172
+ once the push succeeds. On rubygems.org, open
173
+ [pending trusted publishers](https://rubygems.org/profile/oidc/pending_trusted_publishers)
174
+ -> **Create**, with:
175
+
176
+ | Field | Value |
177
+ | --- | --- |
178
+ | RubyGem name | `omniauth-shopline` |
179
+ | Repository owner | `dropstream` |
180
+ | Repository name | `omniauth-shopline` |
181
+ | Workflow filename | `release.yml` |
182
+ | Environment | `release` |
183
+
184
+ The repo also needs a GitHub environment named `release` (Settings -> Environments ->
185
+ New environment), because the publishing job runs in it.
186
+
187
+ After the first successful push the pending publisher becomes an ordinary trusted
188
+ publisher, managed from the gem's own **Trusted publishers** page. It only needs
189
+ revisiting if the gem is renamed or the repository moves.
190
+
191
+ ## Contributing
192
+
193
+ Bug reports and pull requests are welcome on GitHub at
194
+ https://github.com/dropstream/omniauth-shopline.
195
+
196
+ ## License
197
+
198
+ Available as open source under the terms of the [MIT License](LICENSE.txt).
@@ -0,0 +1,245 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "openssl"
5
+ require "securerandom"
6
+ require "uri"
7
+
8
+ require "omniauth-oauth2"
9
+
10
+ module OmniAuth
11
+ module Strategies
12
+ # OmniAuth strategy for SHOPLINE's OAuth 2.0 app authorization flow.
13
+ #
14
+ # https://developer.shopline.com/docs/apps/api-instructions-for-use/app-authorization/
15
+ #
16
+ # SHOPLINE deviates from plain OAuth 2 in three ways that this strategy has to
17
+ # handle itself rather than inherit:
18
+ #
19
+ # 1. The authorize endpoint is a hash-routed admin page, so its query string sits
20
+ # *after* the `#` fragment and cannot be built by `OAuth2::Client#authorize_url`.
21
+ # 2. There is no `state` parameter. `customField` is the documented pass-through,
22
+ # so the CSRF nonce travels in it (see #verify_state).
23
+ # 3. The token endpoint authenticates with `appkey`/`timestamp`/`sign` headers
24
+ # instead of client credentials, and signs POSTs over the request body.
25
+ class Shopline < OmniAuth::Strategies::OAuth2
26
+ SITE_TEMPLATE = "https://%<handle>s.myshopline.com"
27
+
28
+ # The placeholder is replaced with the configured handle unless the consumer
29
+ # supplies a site of their own.
30
+ DEFAULT_SITE = "https://{handle}.myshopline.com"
31
+
32
+ STATE_SESSION_KEY = "omniauth.shopline.state"
33
+
34
+ option :name, "shopline"
35
+
36
+ option :client_options, {
37
+ site: DEFAULT_SITE,
38
+ authorize_url: "/admin/oauth-web/#/oauth/authorize",
39
+ token_url: "/admin/oauth/token/create"
40
+ }
41
+
42
+ # SHOPLINE calls these the app key and app secret. They are the OAuth2
43
+ # client_id/client_secret positional arguments, and may also be passed by name.
44
+ option :app_key, nil
45
+ option :app_secret, nil
46
+
47
+ # SHOPLINE never sends `state`, so OmniAuth::Strategies::OAuth2's own check can
48
+ # never pass. Leave this alone and use :verify_state / :verify_signature below.
49
+ option :provider_ignores_state, true
50
+
51
+ # Round-trips a CSRF nonce through SHOPLINE's `customField` parameter.
52
+ option :verify_state, true
53
+
54
+ # Verifies the `sign` SHOPLINE appends to the callback query string.
55
+ option :verify_signature, true
56
+
57
+ attr_reader :handle
58
+
59
+ def initialize(app, *args, &block)
60
+ super
61
+
62
+ @handle = options[:handle] || raise(ArgumentError, "handle is required")
63
+
64
+ site = options.client_options[:site]
65
+ return unless site.nil? || site.to_s.include?("{handle}")
66
+
67
+ options.client_options[:site] = format(SITE_TEMPLATE, handle: @handle)
68
+ end
69
+
70
+ def app_key
71
+ options.app_key || options.client_id
72
+ end
73
+
74
+ def app_secret
75
+ options.app_secret || options.client_secret
76
+ end
77
+
78
+ # The authorize URL is built by hand: `/admin/oauth-web/#/oauth/authorize` is a
79
+ # client-side route, so the query string belongs after the fragment. Handing the
80
+ # path to `OAuth2::Client#authorize_url` would produce
81
+ # `/admin/oauth-web/?appKey=...#/oauth/authorize`, which the page never reads.
82
+ def authorize_url
83
+ client_options = options.client_options
84
+ query = URI.encode_www_form(authorize_params_for_shopline)
85
+
86
+ "#{client_options[:site]}#{client_options[:authorize_url]}?#{query}"
87
+ end
88
+
89
+ def request_phase
90
+ redirect authorize_url
91
+ end
92
+
93
+ def callback_phase
94
+ error = request.params["error"].to_s
95
+ unless error.empty?
96
+ return fail!(error.to_sym, CallbackError.new(error, request.params["error_description"]))
97
+ end
98
+
99
+ return fail!(:csrf_detected, CallbackError.new(:csrf_detected, "CSRF detected")) unless valid_state?
100
+
101
+ unless valid_signature?
102
+ return fail!(:invalid_signature, CallbackError.new(:invalid_signature, "Signature verification failed"))
103
+ end
104
+
105
+ super
106
+ end
107
+
108
+ # POST /admin/oauth/token/create, authenticated by the appkey/timestamp/sign
109
+ # headers rather than by client credentials, and answered with the token nested
110
+ # under `data`.
111
+ def build_access_token
112
+ timestamp = current_timestamp
113
+ body = JSON.generate(code: request.params["code"])
114
+
115
+ response = client.request(
116
+ :post,
117
+ options.client_options[:token_url],
118
+ body: body,
119
+ headers: {
120
+ "Content-Type" => "application/json",
121
+ "appkey" => app_key.to_s,
122
+ "timestamp" => timestamp.to_s,
123
+ "sign" => signature_for_body(body, timestamp)
124
+ }
125
+ )
126
+
127
+ access_token_from(response)
128
+ end
129
+
130
+ # OmniAuth::Strategy#callback_url appends the callback request's own query
131
+ # string, which would send those params to SHOPLINE as part of redirect_uri and
132
+ # fail the registered-URL match. Note that omniauth 2's `callback_path` already
133
+ # carries SCRIPT_NAME, so mount prefixes must not be added again.
134
+ def callback_url
135
+ options[:callback_url] || options[:redirect_uri] || (full_host + callback_path)
136
+ end
137
+
138
+ # OmniAuth merges this with OmniAuth::Strategies::OAuth2's own credentials block
139
+ # (token, expires_at, expires) rather than replacing it.
140
+ credentials do
141
+ {"scope" => access_token.params["scope"]}.compact
142
+ end
143
+
144
+ extra do
145
+ {
146
+ "handle" => handle,
147
+ "scope" => access_token.params["scope"]
148
+ }
149
+ end
150
+
151
+ private
152
+
153
+ def authorize_params_for_shopline
154
+ params = {
155
+ appKey: app_key,
156
+ responseType: "code",
157
+ scope: options.scope,
158
+ redirectUri: callback_url
159
+ }
160
+
161
+ params[:customField] = new_state if options.verify_state
162
+ params.compact
163
+ end
164
+
165
+ # SHOPLINE has no `state` parameter, but `customField` is passed through to the
166
+ # callback untouched, so it carries the nonce.
167
+ def new_state
168
+ state = SecureRandom.hex(24)
169
+ session[STATE_SESSION_KEY] = state
170
+ state
171
+ end
172
+
173
+ def valid_state?
174
+ return true unless options.verify_state
175
+
176
+ expected = session.delete(STATE_SESSION_KEY)
177
+ !expected.to_s.empty? && secure_compare(request.params["customField"], expected)
178
+ end
179
+
180
+ # SHOPLINE signs the callback query the same way it signs any GET: the params
181
+ # other than `sign`, sorted by key and joined as `k=v&k=v`.
182
+ # https://developer.shopline.com/docs/apps/api-instructions-for-use/generate-and-verify-signatures/
183
+ def valid_signature?
184
+ return true unless options.verify_signature
185
+
186
+ received = request.GET["sign"]
187
+ return false if received.to_s.empty?
188
+
189
+ secure_compare(received, signature_for_query(request.GET))
190
+ end
191
+
192
+ def access_token_from(response)
193
+ parsed = response.parsed
194
+ data = parsed["data"] if parsed.is_a?(Hash)
195
+ token = data["accessToken"] if data.is_a?(Hash)
196
+
197
+ raise ::OAuth2::Error, response if token.to_s.empty?
198
+
199
+ ::OAuth2::AccessToken.new(
200
+ client,
201
+ token,
202
+ "expires_at" => data["expireTime"],
203
+ "scope" => data["scope"]
204
+ )
205
+ end
206
+
207
+ # Milliseconds since the epoch, as SHOPLINE's `timestamp` header requires.
208
+ def current_timestamp
209
+ (Time.now.to_f * 1000).to_i
210
+ end
211
+
212
+ # SHOPLINE signs a POST over the request body concatenated with the timestamp --
213
+ # not the sorted-parameter scheme used for GETs. The body passed here must be the
214
+ # exact string that is sent.
215
+ # https://github.com/shoplineos/shopline-sdk-go/blob/main/client/sign.go
216
+ def signature_for_body(body, timestamp)
217
+ hmac_sha256("#{body}#{timestamp}")
218
+ end
219
+
220
+ def signature_for_query(params)
221
+ source = params.reject { |key, _| key.to_s == "sign" }
222
+ .sort_by { |key, _| key.to_s }
223
+ .map { |key, value| "#{key}=#{value}" }
224
+ .join("&")
225
+
226
+ hmac_sha256(source)
227
+ end
228
+
229
+ def hmac_sha256(source)
230
+ secret = app_secret.to_s
231
+ raise ArgumentError, "app_secret is required to sign SHOPLINE requests" if secret.empty?
232
+
233
+ OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), secret, source)
234
+ end
235
+
236
+ def secure_compare(received, expected)
237
+ received = received.to_s
238
+ expected = expected.to_s
239
+ return false unless received.bytesize == expected.bytesize
240
+
241
+ OpenSSL.fixed_length_secure_compare(received, expected)
242
+ end
243
+ end
244
+ end
245
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Omniauth
4
+ module Shopline
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "omniauth-shopline/version"
4
+ require "omniauth/strategies/shopline"
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path("lib", __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require "omniauth-shopline/version"
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = "omniauth-shopline"
9
+ spec.version = Omniauth::Shopline::VERSION
10
+ spec.authors = ["Dropstream"]
11
+ spec.email = ["karl.falconer@getdropstream.com"]
12
+
13
+ spec.summary = "OmniAuth strategy for SHOPLINE"
14
+ spec.description = "In this gem you will find an OmniAuth SHOPLINE strategy"
15
+ spec.homepage = "https://github.com/dropstream/omniauth-shopline"
16
+ spec.license = "MIT"
17
+
18
+ spec.required_ruby_version = ">= 3.2"
19
+
20
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
21
+ spec.metadata["homepage_uri"] = spec.homepage
22
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/master/CHANGELOG.md"
23
+ spec.metadata["rubygems_mfa_required"] = "true"
24
+
25
+ # Ship only the library itself; development scaffolding stays out of the gem.
26
+ spec.files = Dir.chdir(__dir__) do
27
+ `git ls-files -z`.split("\x0").reject do |f|
28
+ f.match(%r{\A(?:spec|bin|pkg|gemfiles|\.github)/}) ||
29
+ f.match(%r{\A(?:\.gitignore|\.rspec|\.ruby-version|\.ruby-gemset|Gemfile|Gemfile\.lock|Rakefile)\z})
30
+ end
31
+ end
32
+ spec.require_paths = ["lib"]
33
+
34
+ # Deliberately loose. This is a plugin: a ceiling here becomes a ceiling in every
35
+ # consuming application, so it allows the next major of both gems rather than
36
+ # blocking upgrades on the day one ships. The floors are where OmniAuth 2 support
37
+ # actually begins -- omniauth-oauth2 1.8.0 is its first release to require
38
+ # `omniauth ~> 2.0` -- and CI runs the suite against those floors as well as against
39
+ # the newest resolvable versions.
40
+ spec.add_dependency "omniauth", ">= 2.0", "< 4"
41
+ spec.add_dependency "omniauth-oauth2", ">= 1.8", "< 3"
42
+ end
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: omniauth-shopline
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Dropstream
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: omniauth
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '2.0'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '4'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '2.0'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '4'
32
+ - !ruby/object:Gem::Dependency
33
+ name: omniauth-oauth2
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '1.8'
39
+ - - "<"
40
+ - !ruby/object:Gem::Version
41
+ version: '3'
42
+ type: :runtime
43
+ prerelease: false
44
+ version_requirements: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '1.8'
49
+ - - "<"
50
+ - !ruby/object:Gem::Version
51
+ version: '3'
52
+ description: In this gem you will find an OmniAuth SHOPLINE strategy
53
+ email:
54
+ - karl.falconer@getdropstream.com
55
+ executables: []
56
+ extensions: []
57
+ extra_rdoc_files: []
58
+ files:
59
+ - CHANGELOG.md
60
+ - LICENSE.txt
61
+ - README.md
62
+ - lib/omniauth-shopline.rb
63
+ - lib/omniauth-shopline/version.rb
64
+ - lib/omniauth/strategies/shopline.rb
65
+ - omniauth-shopline.gemspec
66
+ homepage: https://github.com/dropstream/omniauth-shopline
67
+ licenses:
68
+ - MIT
69
+ metadata:
70
+ allowed_push_host: https://rubygems.org
71
+ homepage_uri: https://github.com/dropstream/omniauth-shopline
72
+ changelog_uri: https://github.com/dropstream/omniauth-shopline/blob/master/CHANGELOG.md
73
+ rubygems_mfa_required: 'true'
74
+ rdoc_options: []
75
+ require_paths:
76
+ - lib
77
+ required_ruby_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '3.2'
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ requirements: []
88
+ rubygems_version: 3.6.9
89
+ specification_version: 4
90
+ summary: OmniAuth strategy for SHOPLINE
91
+ test_files: []