@spree/docs 0.1.67 → 0.1.69

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.
@@ -21,7 +21,7 @@ This will tell Spree to use your `AdminUser` model for the admin panel. You will
21
21
  include Spree::UserMethods
22
22
  ```
23
23
 
24
- In your `config/initializers/routes.rb` file, you will need define devise routes for the 2nd model:
24
+ In your `config/initializers/routes.rb` file, you will need to define devise routes for the 2nd model:
25
25
 
26
26
  ```ruby
27
27
  Spree::Core::Engine.routes.prepend_routes do
@@ -70,7 +70,7 @@ Once you have added the gem, it's time to bundle:
70
70
  bundle install
71
71
  ```
72
72
 
73
- Finally, let's run the `spree_simple_sales` install generator to copy over the migration we just created answer **yes** if prompted to run migrations:
73
+ Finally, let's run the `spree_simple_sales` install generator to copy over the migration we just created. Answer **yes** if prompted to run migrations:
74
74
 
75
75
  ```bash
76
76
  # context: Your Spree store's app root (i.e. Rails.root); not the extension's root path.
@@ -24,7 +24,7 @@ Now, run the generator to set up Spree integration with Devise:
24
24
  bin/rails g spree:authentication:devise
25
25
  ```
26
26
 
27
- This will create the a new file in `lib/spree/authentication_helpers.rb` that serves as a bridge between Spree and your existing authentication system routes. You can then use this file to customize the routes to your liking. It should automatically pick up standard Devise routes.
27
+ This will create a new file in `lib/spree/authentication_helpers.rb` that serves as a bridge between Spree and your existing authentication system routes. You can then use this file to customize the routes to your liking. It should automatically pick up standard Devise routes.
28
28
 
29
29
  Secondly, this generator will add necessary modules to your `User` model.
30
30
 
@@ -68,7 +68,7 @@ Now, run the generator to set up Spree integration with your custom authenticati
68
68
  bin/rails g spree:authentication:custom
69
69
  ```
70
70
 
71
- This will create the a new file in `lib/spree/authentication_helpers.rb` that serves as a bridge between Spree and your existing authentication system routes. You will need to customize this file to fit your needs.
71
+ This will create a new file in `lib/spree/authentication_helpers.rb` that serves as a bridge between Spree and your existing authentication system routes. You will need to customize this file to fit your needs.
72
72
 
73
73
  Secondly, this generator will add necessary modules to your `User` model.
74
74
 
@@ -10,7 +10,7 @@ The Spree checkout process has been designed for maximum flexibility. It's been
10
10
 
11
11
  ## The Checkout Flow DSL
12
12
 
13
- Spree comes with a new checkout DSL that allows you succinctly define the different steps of your checkout. This new DSL allows you to customize _just_ the checkout flow, while maintaining the unrelated admin states, such as "canceled" and "resumed", that an order can transition to. Ultimately, it provides a shorter syntax compared with overriding the entire state machine for the `Spree::Order` class.
13
+ Spree comes with a new checkout DSL that allows you to succinctly define the different steps of your checkout. This new DSL allows you to customize _just_ the checkout flow, while maintaining the unrelated admin states, such as "canceled" and "resumed", that an order can transition to. Ultimately, it provides a shorter syntax compared with overriding the entire state machine for the `Spree::Order` class.
14
14
 
15
15
  The default checkout flow for Spree is defined like this, adequately demonstrating the abilities of this new system:
16
16
 
@@ -10,7 +10,7 @@ The Spree checkout process has been designed for maximum flexibility. It's been
10
10
 
11
11
  ## The Checkout Flow DSL
12
12
 
13
- Spree comes with a new checkout DSL that allows you succinctly define the different steps of your checkout. This new DSL allows you to customize _just_ the checkout flow, while maintaining the unrelated admin states, such as "canceled" and "resumed", that an order can transition to. Ultimately, it provides a shorter syntax compared with overriding the entire state machine for the `Spree::Order` class.
13
+ Spree comes with a new checkout DSL that allows you to succinctly define the different steps of your checkout. This new DSL allows you to customize _just_ the checkout flow, while maintaining the unrelated admin states, such as "canceled" and "resumed", that an order can transition to. Ultimately, it provides a shorter syntax compared with overriding the entire state machine for the `Spree::Order` class.
14
14
 
15
15
  The default checkout flow for Spree is defined like this, adequately demonstrating the abilities of this new system:
16
16
 
@@ -0,0 +1,301 @@
1
+ ---
2
+ title: Integrate a Third-Party Identity Provider
3
+ description: Step-by-step guide to plugging a custom identity provider (Auth0, Okta, Firebase, Cognito, or any JWT issuer) into the Spree Store API.
4
+ ---
5
+
6
+ import { Since } from '/snippets/since.mdx';
7
+
8
+
9
+ ## Overview
10
+
11
+ Spree's Store API ships with a pluggable authentication system. You register a **strategy class** for a named provider, and the existing login endpoints will dispatch to it — no controller patching, no route overrides, no fork.
12
+
13
+ By the end of this guide you'll have:
14
+
15
+ - A custom strategy that verifies a third-party JWT against a JWKS endpoint
16
+ - A user account auto-provisioned on first login, reused on subsequent logins
17
+ - A standard **Spree-issued JWT + refresh token** returned to the client
18
+ - All Store API endpoints (cart, checkout, account) protected by Spree's own JWT — the third-party token is only used at the exchange step
19
+
20
+ > **INFO:** The same pattern works for Admin API integrations via `admin_authentication_strategies`. Examples in this guide target the Store API; substitute `Spree.admin_user_class` and the admin endpoints where noted.
21
+
22
+ ## Architecture
23
+
24
+ ```
25
+ Client → POST /api/v3/store/auth/login
26
+ { provider: "my_idp", token: "<third-party JWT>" }
27
+
28
+
29
+ AuthController looks up the strategy by `provider` key
30
+
31
+
32
+ YourStrategy#authenticate
33
+ • verifies the JWT against the IdP's JWKS
34
+ • finds or creates a Spree::UserIdentity → Spree user
35
+ • returns success(user) or failure(message)
36
+
37
+
38
+ Spree issues its own JWT (HS256, iss=spree, aud=store_api)
39
+ + a rotatable RefreshToken
40
+
41
+
42
+ Client → subsequent calls with `Authorization: Bearer <Spree JWT>`
43
+ ```
44
+
45
+ The third-party JWT proves identity **once, at login**. After that, the client uses the Spree JWT for everything, and `/auth/refresh` rotates it via Spree's own refresh-token mechanism. Your existing CanCanCan rules, `current_user`, and serializer params just work.
46
+
47
+ ## Step 1: Create the Strategy Class
48
+
49
+ Subclass `Spree::Authentication::Strategies::BaseStrategy` and implement two methods: `provider` (a string identifier) and `authenticate` (returns a `Spree::ServiceModule::Result`).
50
+
51
+ ```ruby app/models/my_app/auth/external_jwt_strategy.rb
52
+ module MyApp
53
+ module Auth
54
+ class ExternalJwtStrategy < Spree::Authentication::Strategies::BaseStrategy
55
+ PROVIDER = 'external_idp'.freeze
56
+
57
+ def provider
58
+ PROVIDER
59
+ end
60
+
61
+ def authenticate
62
+ token = params[:token] || extract_bearer
63
+ return failure(I18n.t('spree.api.unauthorized')) if token.blank?
64
+
65
+ payload = verify_with_jwks(token)
66
+
67
+ user = find_or_create_user_from_oauth(
68
+ provider: PROVIDER,
69
+ uid: payload.fetch('sub'),
70
+ info: {
71
+ email: payload['email'],
72
+ first_name: payload['given_name'],
73
+ last_name: payload['family_name']
74
+ }
75
+ )
76
+
77
+ success(user)
78
+ rescue JWT::DecodeError, JWT::ExpiredSignature, JWT::InvalidIssuerError, JWT::InvalidAudError, KeyError => e
79
+ failure(e.message)
80
+ end
81
+
82
+ private
83
+
84
+ def verify_with_jwks(token)
85
+ jwks_loader = ->(opts) { jwks(force: opts[:invalidate]) }
86
+
87
+ JWT.decode(
88
+ token, nil, true,
89
+ algorithms: ['RS256'],
90
+ iss: ENV.fetch('EXTERNAL_IDP_ISSUER'),
91
+ aud: ENV.fetch('EXTERNAL_IDP_AUDIENCE'),
92
+ verify_iss: true,
93
+ verify_aud: true,
94
+ jwks: jwks_loader
95
+ ).first
96
+ end
97
+
98
+ def jwks(force: false)
99
+ Rails.cache.fetch('external_idp:jwks', expires_in: 1.hour, force: force) do
100
+ uri = URI(ENV.fetch('EXTERNAL_IDP_JWKS_URL'))
101
+ JSON.parse(Net::HTTP.get(uri))
102
+ end
103
+ end
104
+
105
+ def extract_bearer
106
+ header = request_env['HTTP_AUTHORIZATION'].to_s
107
+ header.start_with?('Bearer ') ? header.split(' ', 2).last : nil
108
+ end
109
+ end
110
+ end
111
+ end
112
+ ```
113
+
114
+ ### What the base class gives you
115
+
116
+ `Spree::Authentication::Strategies::BaseStrategy` (in `spree_core`) exposes a few helpers so your subclass stays small:
117
+
118
+ | Helper | Purpose |
119
+ |---|---|
120
+ | `success(user)` | Wrap a user in a successful `ServiceModule::Result` |
121
+ | `failure(message)` | Wrap an error message in a failed result |
122
+ | `find_user_by_email(email)` | Lookup against `Spree.user_class` |
123
+ | `find_or_create_user_from_oauth(provider:, uid:, info:, tokens: {})` | Calls `Spree::UserIdentity.find_or_create_from_oauth` with the right `user_class` |
124
+ | `params`, `request_env`, `user_class` | Reader access to the controller-supplied inputs |
125
+
126
+ `find_or_create_user_from_oauth` returns the **user**, not the identity. It creates the `Spree::UserIdentity` row on first login (mapping `provider + uid → user`) and reuses it on subsequent logins — so repeat sign-ins land on the same Spree customer.
127
+
128
+ ## Step 2: Register the Strategy
129
+
130
+ Add the strategy to `Spree.store_authentication_strategies` in an initializer. The key you choose here is what clients will send as `provider` in the login payload.
131
+
132
+ ```ruby config/initializers/spree.rb
133
+ Rails.application.config.after_initialize do
134
+ Spree.store_authentication_strategies.add(:external_idp, MyApp::Auth::ExternalJwtStrategy)
135
+ end
136
+ ```
137
+
138
+ `Spree.store_authentication_strategies` is a `Spree::Authentication::StrategyRegistry`. The full API:
139
+
140
+ | Method | Purpose |
141
+ |---|---|
142
+ | `add(key, strategy_class)` | Register a strategy. Overwrites any existing entry under the same key — that's how you swap the built-in `:email` strategy. |
143
+ | `remove(key)` | Unregister a strategy. Idempotent (returns `nil` if the key was never registered). |
144
+ | `[key]` | Look up a registered class. |
145
+ | `key?(key)`, `keys`, `values`, `each`, `to_h` | Standard introspection. |
146
+
147
+ > **WARNING:** `Spree::UserIdentity` validates that `provider` is a registered strategy key. Registration must happen during boot — before the first login attempt.
148
+
149
+ For an admin-side equivalent, register under `Spree.admin_authentication_strategies` instead and instantiate your strategy against `Spree.admin_user_class`:
150
+
151
+ ```ruby
152
+ Spree.admin_authentication_strategies.add(:okta, MyApp::Auth::OktaStrategy)
153
+ ```
154
+
155
+ ## Step 3: Call the Exchange Endpoint
156
+
157
+ `POST /api/v3/store/auth/login` is the single dispatcher. The `provider` field in the body selects the strategy — omit it for built-in email/password, set it to your registered key for everything else. The remaining body fields are whatever your strategy reads from `params`.
158
+
159
+ ```http
160
+ POST /api/v3/store/auth/login
161
+ X-Spree-API-Key: pk_your_publishable_key
162
+ Content-Type: application/json
163
+
164
+ {
165
+ "provider": "external_idp",
166
+ "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIs..."
167
+ }
168
+ ```
169
+
170
+ The response is the standard Spree auth payload:
171
+
172
+ ```json
173
+ {
174
+ "token": "<Spree HS256 JWT, aud=store_api>",
175
+ "refresh_token": "rt_xxxxxxxxxxxx",
176
+ "user": { "id": "user_...", "email": "...", "first_name": "...", "...": "..." }
177
+ }
178
+ ```
179
+
180
+ From here, the client sends `Authorization: Bearer <Spree JWT>` on every subsequent Store API call. When the JWT expires (default: 1 hour), the client hits `POST /api/v3/store/auth/refresh` with the refresh token to rotate both.
181
+
182
+ From `@spree/sdk` the same call looks like:
183
+
184
+ ```ts
185
+ import { createClient } from '@spree/sdk'
186
+
187
+ const client = createClient({ baseUrl: 'https://your-store.com', publishableKey: '<pk>' })
188
+
189
+ const auth = await client.auth.login({
190
+ provider: 'external_idp',
191
+ token: thirdPartyJwt,
192
+ })
193
+ ```
194
+
195
+ `LoginCredentials` is a discriminated union — pass `{ email, password }` for the built-in strategy, or `{ provider, ...customFields }` for any strategy you registered.
196
+
197
+ ## Account Linking
198
+
199
+ The naive flow above will create a brand new Spree user the first time a given `(provider, uid)` is seen — even if a user with the same email already exists from a password signup. If you want **same-email-means-same-customer**, look up by email first and attach an identity to the existing user:
200
+
201
+ ```ruby
202
+ def authenticate
203
+ payload = verify_with_jwks(params[:token])
204
+ email = payload.fetch('email')
205
+
206
+ user = find_user_by_email(email)
207
+
208
+ if user
209
+ user.identities.find_or_create_by!(provider: PROVIDER, uid: payload['sub']) do |identity|
210
+ identity.info = { email: email, name: payload['name'] }
211
+ end
212
+ else
213
+ user = find_or_create_user_from_oauth(
214
+ provider: PROVIDER,
215
+ uid: payload['sub'],
216
+ info: { email: email, first_name: payload['given_name'], last_name: payload['family_name'] }
217
+ )
218
+ end
219
+
220
+ success(user)
221
+ end
222
+ ```
223
+
224
+ > **WARNING:** **Only link by email if your IdP guarantees `email_verified`.** Silent linking against an unverified email is a known account-takeover vector: an attacker registers `victim@example.com` at the IdP without proving ownership, then logs into Spree as the real victim. Check the `email_verified` claim (or equivalent) before linking, and reject otherwise.
225
+
226
+ ## Logout
227
+
228
+ ```http
229
+ POST /api/v3/store/auth/logout
230
+ Content-Type: application/json
231
+
232
+ { "refresh_token": "rt_xxxxxxxxxxxx" }
233
+ ```
234
+
235
+ This revokes the Spree refresh token. The Spree JWT itself remains valid until it expires naturally (short-lived by design — default 1 hour). Spree does **not** call the IdP's revocation endpoint; if you need single sign-out, do that from the client.
236
+
237
+ ## Security Notes
238
+
239
+ A few things worth getting right:
240
+
241
+ - **Don't try to pass the third-party JWT through to protected endpoints.** Spree's `JwtAuthentication` concern verifies `iss: 'spree'` and `aud: 'store_api'` with HS256 against the Spree secret — a foreign RS256 token will never validate, and you don't want it to. The exchange-at-login model is the right one.
242
+ - **JWKS caching and rotation.** Cache the JWKS (the example uses a 1-hour TTL) but make sure your loader honors the `invalidate: true` option so that an unrecognized `kid` triggers a refetch. Otherwise key rotation at the IdP locks users out for up to the TTL.
243
+ - **Validate `iss` and `aud` claims.** Always. The example passes `verify_iss: true, verify_aud: true` to `JWT.decode` — don't drop those.
244
+ - **Algorithm pinning.** Hard-code `algorithms: ['RS256']` (or whatever your IdP uses). Never let the token's own `alg` header decide — the classic `alg: none` and HS-as-RS confusion attacks both exploit lax algorithm selection.
245
+ - **Rate limiting.** `POST /auth/login` is rate-limited per IP via `Spree::Api::Config[:rate_limit_login]`. Tune it in your app config if needed — the same limit applies to email/password and provider-dispatched logins.
246
+
247
+ ## Testing
248
+
249
+ A strategy is a plain Ruby class — test it in isolation without booting a controller:
250
+
251
+ ```ruby spec/models/my_app/auth/external_jwt_strategy_spec.rb
252
+ require 'rails_helper'
253
+
254
+ RSpec.describe MyApp::Auth::ExternalJwtStrategy do
255
+ let(:rsa_private) { OpenSSL::PKey::RSA.generate(2048) }
256
+ let(:jwks) { { keys: [JWT::JWK.new(rsa_private).export] } }
257
+
258
+ let(:token) do
259
+ JWT.encode(
260
+ { sub: 'idp-user-123', email: 'alice@example.com', iss: 'https://idp.example', aud: 'spree' },
261
+ rsa_private, 'RS256'
262
+ )
263
+ end
264
+
265
+ before do
266
+ stub_request(:get, ENV['EXTERNAL_IDP_JWKS_URL']).to_return(body: jwks.to_json)
267
+ end
268
+
269
+ subject(:result) do
270
+ described_class.new(
271
+ params: { provider: 'external_idp', token: token },
272
+ request_env: {}
273
+ ).authenticate
274
+ end
275
+
276
+ it 'provisions a Spree user on first login' do
277
+ expect { result }.to change(Spree.user_class, :count).by(1)
278
+ expect(result).to be_success
279
+ expect(result.value.email).to eq('alice@example.com')
280
+ end
281
+
282
+ it 'reuses the user on subsequent logins' do
283
+ described_class.new(params: { token: token }, request_env: {}).authenticate
284
+ expect { result }.not_to change(Spree.user_class, :count)
285
+ end
286
+
287
+ it 'fails on an expired token' do
288
+ expired = JWT.encode({ sub: 'x', exp: 1.hour.ago.to_i, iss: 'https://idp.example', aud: 'spree' }, rsa_private, 'RS256')
289
+ result = described_class.new(params: { token: expired }, request_env: {}).authenticate
290
+ expect(result).not_to be_success
291
+ end
292
+ end
293
+ ```
294
+
295
+ ## Reference
296
+
297
+ - `Spree::Authentication::Strategies::BaseStrategy` — `spree/core/app/models/spree/authentication/strategies/base_strategy.rb`
298
+ - `Spree::UserIdentity` — `spree/core/app/models/spree/user_identity.rb`
299
+ - `Spree::Api::V3::Store::AuthController` — `spree/api/app/controllers/spree/api/v3/store/auth_controller.rb`
300
+ - `Spree::Api::V3::JwtAuthentication` — `spree/api/app/controllers/concerns/spree/api/v3/jwt_authentication.rb`
301
+ - See also: [Authentication](../customization/authentication.md) for `Spree.user_class` integration.
@@ -39,7 +39,7 @@ If you used that gem in the past you need to remove it. Multi-Currency is now in
39
39
 
40
40
  All international configuration is now kept on the `Store` model in the database rather than in initializer files.
41
41
 
42
- If you used `spree_i18n` gem before please remove any `SpreeI18n::Config`references from your `config/initializers/spree.rb` file.
42
+ If you used `spree_i18n` gem before please remove any `SpreeI18n::Config` references from your `config/initializers/spree.rb` file.
43
43
 
44
44
  ## Optional Add `deface` gem
45
45
 
@@ -71,9 +71,9 @@ This rake task will enqueue background jobs to populate the product metrics for
71
71
 
72
72
  ### Replace `auto_strip_attributes` gem usage
73
73
 
74
- `auto_strip_attributes` gem [was removed from Spree](https://github.com/spree/spree/pull/13462) due to bugs and conflicts with translations feature. Also it's not required anymore as built-in Rails `normalizes` provides the same feature without an additional depepdency.
74
+ `auto_strip_attributes` gem [was removed from Spree](https://github.com/spree/spree/pull/13462) due to bugs and conflicts with translations feature. Also it's not required anymore as built-in Rails `normalizes` provides the same feature without an additional dependency.
75
75
 
76
- If you used `auto_stripe_attributes` in your application, you will need to change it to `normalizez`, eg.
76
+ If you used `auto_strip_attributes` in your application, you will need to change it to `normalizes`, eg.
77
77
 
78
78
  Replace
79
79
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spree/docs",
3
- "version": "0.1.67",
3
+ "version": "0.1.69",
4
4
  "description": "Spree Commerce developer documentation for AI agents and local reference",
5
5
  "type": "module",
6
6
  "license": "CC-BY-4.0",