Skip to main content

Overview

Both Spree APIs — the customer-facing Store API and the back-office Admin API — ship with the same pluggable authentication system. You register a strategy class for a named provider, and the existing login endpoints 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
  • Every API endpoint protected by Spree’s own JWT — the third-party token is only used at the login exchange step

Store vs Admin — what changes

The mechanism is identical for both surfaces. A strategy you write works on either side; only the registry you add it to (and a handful of surface-specific details) differ: Everything else — the strategy class you write, the BaseStrategy helpers, JWT verification, account provisioning, account linking — is the same. The walkthrough below uses the Store API; each step calls out the one-line Admin swap.

Architecture

The flow below uses the Store API; the Admin API is identical with admin in the path and aud: admin_api on the issued JWT.
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.

Step 1: Create the Strategy Class

Subclass Spree::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: 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 to a registry in an initializer. This is the only line that decides which API the provider servesstore_authentication_strategies for customer login, admin_authentication_strategies for staff login. Register with both to allow the same provider on either surface. The key you choose here is what clients send as provider in the login payload.
Both registries are a Spree::Authentication::StrategyRegistry with the same API: The strategy is instantiated with the surface’s user class automatically — Spree.user_class from the store registry, Spree.admin_user_class from the admin registry — and your authenticate reads it via the user_class helper, so the same class provisions customers on one side and staff on the other.
Restrict a surface to SSO by removing the built-in email/password strategy after adding yours: Spree.admin_authentication_strategies.remove(:email) locks staff to your provider; the store side stays on email/password.
Spree::UserIdentity validates that provider is a registered strategy key. Registration must happen during boot — before the first login attempt.

Step 3: Call the Exchange Endpoint

The login endpoint is the single dispatcher — /api/v3/store/auth/login for customers, /api/v3/admin/auth/login for staff. 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.
The response is the standard Spree auth payload. The difference is the refresh token: the Store API returns it in the body, while the Admin API sets it as an HttpOnly cookie scoped to /api/v3/admin/auth and omits it from the body.
From here, the client sends Authorization: Bearer <Spree JWT> on every subsequent call. When the JWT expires (default: 1 hour), it rotates via the refresh endpoint — the Store API takes the refresh token in the body of POST /api/v3/store/auth/refresh; the Admin API drives POST /api/v3/admin/auth/refresh entirely from the cookie (see Admin Auth & Cookie Refresh). The SDKs wrap the same exchange — @spree/sdk for the Store API, @spree/admin-sdk for the Admin API:
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:
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.

Logout

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.

Security Notes

A few things worth getting right:
  • Don’t try to pass the third-party JWT through to protected endpoints. Spree’s JwtAuthentication concern verifies iss: 'spree' and the expected audience (store_api or admin_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 kid_not_found: true option so that an unrecognized kid triggers a refetch. Otherwise key rotation at the IdP locks users out for up to the TTL.
  • Validate iss and aud claims. Always. The example passes verify_iss: true, verify_aud: true to JWT.decode — don’t drop those.
  • 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.
  • 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.

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::BaseStrategyspree/core/app/models/spree/authentication/strategies/base_strategy.rb
  • Spree::UserIdentityspree/core/app/models/spree/user_identity.rb
  • Spree::Api::V3::Store::AuthControllerspree/api/app/controllers/spree/api/v3/store/auth_controller.rb
  • Spree::Api::V3::Admin::AuthControllerspree/api/app/controllers/spree/api/v3/admin/auth_controller.rb
  • Spree::Api::V3::JwtAuthenticationspree/api/app/controllers/concerns/spree/api/v3/jwt_authentication.rb
  • See also: Staff & Roles for the admin login flow, Customers for storefront auth, and Authentication for Spree.user_class integration.