Documentation Index
Fetch the complete documentation index at: https://spreecommerce.org/docs/llms.txt
Use this file to discover all available pages before exploring further.
Overview
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. By the end of this guide you’ll have:- A custom strategy that verifies a third-party JWT against a JWKS endpoint
- A user account auto-provisioned on first login, reused on subsequent logins
- A standard Spree-issued JWT + refresh token returned to the client
- All Store API endpoints (cart, checkout, account) protected by Spree’s own JWT — the third-party token is only used at the exchange step
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.Architecture
/auth/refresh rotates it via Spree’s own refresh-token mechanism. Your existing CanCanCan rules, current_user, and serializer params just work.
Step 1: Create the Strategy Class
SubclassSpree::Authentication::Strategies::BaseStrategy and implement two methods: provider (a string identifier) and authenticate (returns a Spree::ServiceModule::Result).
app/models/my_app/auth/external_jwt_strategy.rb
What the base class gives you
Spree::Authentication::Strategies::BaseStrategy (in spree_core) exposes a few helpers so your subclass stays small:
| Helper | Purpose |
|---|---|
success(user) | Wrap a user in a successful ServiceModule::Result |
failure(message) | Wrap an error message in a failed result |
find_user_by_email(email) | Lookup against Spree.user_class |
find_or_create_user_from_oauth(provider:, uid:, info:, tokens: {}) | Calls Spree::UserIdentity.find_or_create_from_oauth with the right user_class |
params, request_env, user_class | Reader access to the controller-supplied inputs |
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.
Step 2: Register the Strategy
Add the strategy toSpree.store_authentication_strategies in an initializer. The key you choose here is what clients will send as provider in the login payload.
config/initializers/spree.rb
Spree.store_authentication_strategies is a Spree::Authentication::StrategyRegistry. The full API:
| Method | Purpose |
|---|---|
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. |
remove(key) | Unregister a strategy. Idempotent (returns nil if the key was never registered). |
[key] | Look up a registered class. |
key?(key), keys, values, each, to_h | Standard introspection. |
Spree.admin_authentication_strategies instead and instantiate your strategy against Spree.admin_user_class:
Step 3: Call the Exchange Endpoint
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.
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.
From @spree/sdk the same call looks like:
LoginCredentials is a discriminated union — pass { email, password } for the built-in strategy, or { provider, ...customFields } for any strategy you registered.
Account Linking
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:
Logout
Security Notes
A few things worth getting right:- Don’t try to pass the third-party JWT through to protected endpoints. Spree’s
JwtAuthenticationconcern verifiesiss: 'spree'andaud: '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. - JWKS caching and rotation. Cache the JWKS (the example uses a 1-hour TTL) but make sure your loader honors the
invalidate: trueoption so that an unrecognizedkidtriggers a refetch. Otherwise key rotation at the IdP locks users out for up to the TTL. - Validate
issandaudclaims. Always. The example passesverify_iss: true, verify_aud: truetoJWT.decode— don’t drop those. - Algorithm pinning. Hard-code
algorithms: ['RS256'](or whatever your IdP uses). Never let the token’s ownalgheader decide — the classicalg: noneand HS-as-RS confusion attacks both exploit lax algorithm selection. - Rate limiting.
POST /auth/loginis rate-limited per IP viaSpree::Api::Config[:rate_limit_login]. Tune it in your app config if needed — the same limit applies to email/password and provider-dispatched logins.
Testing
A strategy is a plain Ruby class — test it in isolation without booting a controller:spec/models/my_app/auth/external_jwt_strategy_spec.rb
Reference
Spree::Authentication::Strategies::BaseStrategy—spree/core/app/models/spree/authentication/strategies/base_strategy.rbSpree::UserIdentity—spree/core/app/models/spree/user_identity.rbSpree::Api::V3::Store::AuthController—spree/api/app/controllers/spree/api/v3/store/auth_controller.rbSpree::Api::V3::JwtAuthentication—spree/api/app/controllers/concerns/spree/api/v3/jwt_authentication.rb- See also: Authentication for
Spree.user_classintegration.

