omniauth-sps_commerce 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: 0acb9eeb0bc851072a548e41b2eba2c6147e3adcc8f37212f989db855dc1d033
4
+ data.tar.gz: 90faa4b21c306e2e962299fa98399992d5adcb4205029e4cb37fad344e817c1f
5
+ SHA512:
6
+ metadata.gz: '0775910b5083a680d252ba95a4686be5aee85edc53451a5c7f3d14c29b14fe8cb80b568be466865302776d8e409cd3d682a6f15000a7cd99b639fcb768b69244'
7
+ data.tar.gz: 485b917d28bf009fec063639f78d87ac91ec7e40bcb6d1b0a6b4b9a5945759abe96b1109377da03f07af761f059662d43706e28928897e236d2637885cb06597
data/CHANGELOG.md ADDED
@@ -0,0 +1,19 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ ## [Unreleased]
6
+
7
+ ### Added
8
+
9
+ - `OmniAuth::Strategies::SpsCommerce`, registered as `:sps_commerce`: the authorization code
10
+ flow against `auth.spscommerce.com` (`/authorize`, `/oauth/token`), with a JSON token
11
+ request carrying the client credentials in its body.
12
+ - Default scope `openid profile email offline_access` and default audience
13
+ `https://spscommerce.com`, so the callback returns an API access token and a refresh token.
14
+ - `uid` and `info` from the id_token, falling back to the access token's claims; `extra`
15
+ carries both claim sets and the token response params (without the raw id_token).
16
+ - Per-request `prompt` and `login_hint`.
17
+ - A `callback_url` override that drops the callback's query string and honors a configured
18
+ `:callback_url`.
19
+ - Supports `omniauth-oauth2` 1.8 as well as 1.9, so apps still on 1.8 can use it.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 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
13
+ all 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
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,102 @@
1
+ # OmniAuth SPS Commerce
2
+
3
+ An OmniAuth strategy for [SPS Commerce](https://www.spscommerce.com). It runs the OAuth 2.0
4
+ authorization code flow against `auth.spscommerce.com` (SPS's Auth0 tenant) and returns an
5
+ access token and refresh token for the SPS APIs, such as the Transaction API.
6
+
7
+ Requires Ruby >= 3.2, OmniAuth 2.x and `omniauth-oauth2` 1.8 or 1.9.
8
+
9
+ ## Installation
10
+
11
+ ```ruby
12
+ gem 'omniauth-sps_commerce'
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ OmniAuth 2 only accepts `POST` for the request phase. In a Rails app, add
18
+ [`omniauth-rails_csrf_protection`](https://github.com/cookpad/omniauth-rails_csrf_protection)
19
+ and link to the provider with `button_to`.
20
+
21
+ ```ruby
22
+ Rails.application.config.middleware.use OmniAuth::Builder do
23
+ provider :sps_commerce, ENV['SPS_CLIENT_ID'], ENV['SPS_CLIENT_SECRET']
24
+ end
25
+ ```
26
+
27
+ Register the callback URL (`https://<host>/auth/sps_commerce/callback`) on the app in the
28
+ SPS Dev Center.
29
+
30
+ ### Production and sandbox keys
31
+
32
+ Each SPS app comes with a production key and a sandbox key. Both authenticate against
33
+ `auth.spscommerce.com`, so register the strategy once per key under different names:
34
+
35
+ ```ruby
36
+ Rails.application.config.middleware.use OmniAuth::Builder do
37
+ provider :sps_commerce, ENV['SPS_CLIENT_ID'], ENV['SPS_CLIENT_SECRET']
38
+ provider :sps_commerce, ENV['SPS_SANDBOX_CLIENT_ID'], ENV['SPS_SANDBOX_CLIENT_SECRET'],
39
+ name: :sps_commerce_sandbox
40
+ end
41
+ ```
42
+
43
+ The sandbox registration answers on `/auth/sps_commerce_sandbox` and
44
+ `/auth/sps_commerce_sandbox/callback`. A refresh token can only be redeemed with the key
45
+ that issued it, so record which registration a token came from (`auth.provider`).
46
+
47
+ Use a separate SPS app (and so separate keys) per deployment environment.
48
+
49
+ ## The auth hash
50
+
51
+ ```ruby
52
+ auth = request.env['omniauth.auth']
53
+ auth['provider'] # "sps_commerce" or "sps_commerce_sandbox"
54
+ auth['uid'] # Auth0 subject of the SPS user who authorized
55
+ auth['info'] # name, email, nickname (from the id_token)
56
+ auth['credentials']['token'] # access token
57
+ auth['credentials']['refresh_token']
58
+ auth['credentials']['expires_at']
59
+ auth['extra']['claims'] # id_token claims
60
+ auth['extra']['access_token_claims']# access token claims, including SPS-specific ones
61
+ ```
62
+
63
+ Token claims are decoded without verifying signatures. That is safe only because tokens
64
+ come straight from `auth.spscommerce.com` over TLS, never from the browser. Don't reuse the
65
+ decoder on tokens from any other source.
66
+
67
+ ## Configuring
68
+
69
+ | Option | Default | Notes |
70
+ |--------|---------|-------|
71
+ | `scope` | `openid profile email offline_access` | `offline_access` is what returns a refresh token. |
72
+ | `audience` | `https://spscommerce.com` | Without an audience, Auth0 issues an opaque token that only works on `/userinfo`. |
73
+ | `client_options[:site]` | `https://auth.spscommerce.com` | |
74
+ | `callback_url` | derived from the request | Drops the callback's query string, since Auth0 requires an exact `redirect_uri` match. |
75
+
76
+ `prompt` and `login_hint` can also be set per request (`/auth/sps_commerce?login_hint=…`).
77
+ Nothing else is taken from request params.
78
+
79
+ The endpoints come from
80
+ [SPS's OpenID configuration](https://auth.spscommerce.com/.well-known/openid-configuration).
81
+
82
+ ## Development
83
+
84
+ Run `bin/setup`, then `bundle exec rake spec`. Run `bundle exec bundle-audit check --update`
85
+ to check the locked dependencies against the advisory database; CI runs it weekly.
86
+
87
+ ## Releasing
88
+
89
+ Releases are cut from the GitHub Releases UI and published by the `Release` workflow
90
+ through [RubyGems trusted publishing](https://guides.rubygems.org/trusted-publishing/):
91
+
92
+ 1. Bump `VERSION` in `lib/omniauth-sps_commerce/version.rb` and add a `CHANGELOG.md`
93
+ section. Commit both on `main`.
94
+ 2. Draft a release targeting `main` with tag `vX.Y.Z` matching `version.rb`, then publish.
95
+
96
+ One-time setup on rubygems.org: open the gem, then **Trusted publishers** → **Create**, with
97
+ owner `dropstream`, repository `omniauth-sps_commerce`, workflow `release.yml` and environment
98
+ `release`. The repo also needs an environment named `release`.
99
+
100
+ ## License
101
+
102
+ Available as open source under the terms of the [MIT License](LICENSE.txt).
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'omniauth-oauth2'
5
+
6
+ module OmniAuth
7
+ module Strategies
8
+ # SPS Commerce identity runs on Auth0. The endpoints below come from
9
+ # https://auth.spscommerce.com/.well-known/openid-configuration.
10
+ #
11
+ # An SPS app has a production key and a sandbox key. Both authenticate against
12
+ # the same host, so the strategy does not need to know which one it holds:
13
+ # register it twice with different names to offer both (see the README).
14
+ class SpsCommerce < OmniAuth::Strategies::OAuth2
15
+ DEFAULT_SITE = 'https://auth.spscommerce.com'
16
+ # The audience SPS documents for the authorize request. Without one, Auth0
17
+ # issues an opaque access token that only works on /userinfo.
18
+ DEFAULT_AUDIENCE = 'https://spscommerce.com'
19
+ # offline_access is what makes Auth0 return a refresh token.
20
+ DEFAULT_SCOPE = 'openid profile email offline_access'
21
+
22
+ option :name, 'sps_commerce'
23
+
24
+ option :client_options, {
25
+ site: DEFAULT_SITE,
26
+ authorize_url: '/authorize',
27
+ token_url: '/oauth/token',
28
+ # SPS documents client_id and client_secret in the token request body.
29
+ auth_scheme: :request_body
30
+ }
31
+
32
+ option :scope, DEFAULT_SCOPE
33
+ option :audience, DEFAULT_AUDIENCE
34
+
35
+ option :authorize_options, %i[scope audience prompt login_hint]
36
+
37
+ # Options a request may set for itself, e.g.
38
+ # /auth/sps_commerce?login_hint=user@example.com
39
+ REQUEST_AUTHORIZE_OPTIONS = %w[prompt login_hint].freeze
40
+
41
+ uid { user_claims['sub'] }
42
+
43
+ info do
44
+ {
45
+ name: user_claims['name'],
46
+ email: user_claims['email'],
47
+ nickname: user_claims['nickname']
48
+ }
49
+ end
50
+
51
+ extra do
52
+ {
53
+ 'params' => access_token.params.except('id_token'),
54
+ 'claims' => user_claims,
55
+ 'access_token_claims' => access_token_claims
56
+ }
57
+ end
58
+
59
+ def authorize_params
60
+ super.tap do |params|
61
+ REQUEST_AUTHORIZE_OPTIONS.each do |key|
62
+ value = request.params[key]
63
+ params[key.to_sym] = value unless value.nil? || value == ''
64
+ end
65
+ end
66
+ end
67
+
68
+ # SPS documents a JSON token request; oauth2 sends a form unless the token
69
+ # params carry this header. It is added here rather than through the
70
+ # token_params option, which would symbolize the header name, and oauth2
71
+ # only recognizes it as a string.
72
+ def build_access_token
73
+ client.auth_code.get_token(
74
+ request.params['code'],
75
+ { redirect_uri: callback_url, headers: { 'Content-Type' => 'application/json' } }
76
+ .merge(token_params.to_hash(symbolize_keys: true)),
77
+ deep_symbolize(options.auth_token_params)
78
+ )
79
+ end
80
+
81
+ # OmniAuth::Strategy#callback_url appends the callback request's own query
82
+ # string, and Auth0 requires redirect_uri to match an allowed callback URL
83
+ # exactly. omniauth 2's `callback_path` already carries SCRIPT_NAME, so mount
84
+ # prefixes must not be added again.
85
+ def callback_url
86
+ options[:callback_url] || (full_host + callback_path)
87
+ end
88
+
89
+ private
90
+
91
+ # Claims for the signed-in user: from the id_token (granted by the openid
92
+ # scope), otherwise from the access token. The payload is read without
93
+ # verifying the signature, which is acceptable only because the token
94
+ # arrives directly from auth.spscommerce.com over TLS, never from the browser.
95
+ def user_claims
96
+ @user_claims ||= decode_jwt_claims(access_token.params['id_token']) || access_token_claims
97
+ end
98
+
99
+ # The API access token's claims, which carry the SPS-specific ones (for
100
+ # example the organization). Empty when the token is opaque.
101
+ def access_token_claims
102
+ @access_token_claims ||= decode_jwt_claims(access_token.token) || {}
103
+ end
104
+
105
+ def decode_jwt_claims(jwt)
106
+ payload = jwt.to_s.split('.')[1]
107
+ return nil if payload.nil? || payload.empty?
108
+
109
+ # Base64url-decode by hand; the base64 gem left Ruby's default gems in 3.4.
110
+ json = (payload.tr('-_', '+/') + '=' * (-payload.length % 4)).unpack1('m')
111
+ claims = JSON.parse(json)
112
+ claims.is_a?(Hash) ? claims : nil
113
+ rescue ArgumentError, JSON::ParserError, EncodingError
114
+ nil
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Omniauth
4
+ module SpsCommerce
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "omniauth-sps_commerce/version"
4
+ require "omniauth/strategies/sps_commerce"
@@ -0,0 +1,38 @@
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-sps_commerce/version"
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = "omniauth-sps_commerce"
9
+ spec.version = Omniauth::SpsCommerce::VERSION
10
+ spec.authors = ["Karl Falconer"]
11
+ spec.email = ["karl.falconer@getdropstream.com"]
12
+
13
+ spec.summary = "OmniAuth strategy for SPS Commerce"
14
+ spec.description = "An OmniAuth OAuth2 strategy that authenticates against SPS Commerce " \
15
+ "(auth.spscommerce.com) and obtains tokens for the SPS Commerce APIs."
16
+ spec.homepage = "https://github.com/dropstream/omniauth-sps_commerce"
17
+ spec.license = "MIT"
18
+
19
+ spec.required_ruby_version = ">= 3.2"
20
+
21
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
22
+ spec.metadata["homepage_uri"] = spec.homepage
23
+ spec.metadata["source_code_uri"] = spec.homepage
24
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
25
+ spec.metadata["rubygems_mfa_required"] = "true"
26
+
27
+ # Ship only the library itself; development scaffolding stays out of the gem.
28
+ spec.files = Dir.chdir(__dir__) do
29
+ `git ls-files -z`.split("\x0").reject do |f|
30
+ f.match(%r{\A(?:spec|bin|pkg|\.github)/}) ||
31
+ f.match(%r{\A(?:\.gitignore|\.rspec|\.ruby-version|\.ruby-gemset|Gemfile|Gemfile\.lock|Rakefile)\z})
32
+ end
33
+ end
34
+ spec.require_paths = ["lib"]
35
+
36
+ spec.add_dependency "omniauth", "~> 2.0"
37
+ spec.add_dependency "omniauth-oauth2", ">= 1.8", "< 2"
38
+ end
metadata ADDED
@@ -0,0 +1,87 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: omniauth-sps_commerce
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Karl Falconer
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
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '2.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: omniauth-oauth2
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '1.8'
33
+ - - "<"
34
+ - !ruby/object:Gem::Version
35
+ version: '2'
36
+ type: :runtime
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '1.8'
43
+ - - "<"
44
+ - !ruby/object:Gem::Version
45
+ version: '2'
46
+ description: An OmniAuth OAuth2 strategy that authenticates against SPS Commerce (auth.spscommerce.com)
47
+ and obtains tokens for the SPS Commerce APIs.
48
+ email:
49
+ - karl.falconer@getdropstream.com
50
+ executables: []
51
+ extensions: []
52
+ extra_rdoc_files: []
53
+ files:
54
+ - CHANGELOG.md
55
+ - LICENSE.txt
56
+ - README.md
57
+ - lib/omniauth-sps_commerce.rb
58
+ - lib/omniauth-sps_commerce/version.rb
59
+ - lib/omniauth/strategies/sps_commerce.rb
60
+ - omniauth-sps_commerce.gemspec
61
+ homepage: https://github.com/dropstream/omniauth-sps_commerce
62
+ licenses:
63
+ - MIT
64
+ metadata:
65
+ allowed_push_host: https://rubygems.org
66
+ homepage_uri: https://github.com/dropstream/omniauth-sps_commerce
67
+ source_code_uri: https://github.com/dropstream/omniauth-sps_commerce
68
+ changelog_uri: https://github.com/dropstream/omniauth-sps_commerce/blob/main/CHANGELOG.md
69
+ rubygems_mfa_required: 'true'
70
+ rdoc_options: []
71
+ require_paths:
72
+ - lib
73
+ required_ruby_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '3.2'
78
+ required_rubygems_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ requirements: []
84
+ rubygems_version: 3.6.9
85
+ specification_version: 4
86
+ summary: OmniAuth strategy for SPS Commerce
87
+ test_files: []