> ## 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.

# Integrate a Third-Party Identity Provider

> Step-by-step guide to plugging a custom identity provider (Auth0, Okta, Firebase, Cognito, or any JWT issuer) into the Spree Store API.

export const Since = ({version, from}) => {
  const knownPrevious = {
    '5.0': '4.10',
    '6.0': '5.4'
  };
  const previous = (from ?? knownPrevious[version]) ?? (() => {
    const [major, minor] = version.split('.').map(Number);
    if (Number.isNaN(major) || Number.isNaN(minor) || minor < 1) {
      throw new Error(`<Since version="${version}" />: cannot derive previous version automatically. ` + `Pass an explicit "from" prop, e.g. <Since version="${version}" from="X.Y" />.`);
    }
    return `${major}.${minor - 1}`;
  })();
  return <Tooltip tip={`Available since Spree ${version}+.`} cta="Upgrade instructions" href={`/developer/upgrades/${previous}-to-${version}`}>
      <Badge icon="lock">Spree {version}+</Badge>
    </Tooltip>;
};

<Since version="5.5" />

## 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

<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.
</Info>

## Architecture

```
Client → POST /api/v3/store/auth/login
            { provider: "my_idp", token: "<third-party JWT>" }
              │
              ▼
   AuthController looks up the strategy by `provider` key
              │
              ▼
   YourStrategy#authenticate
     • verifies the JWT against the IdP's JWKS
     • finds or creates a Spree::UserIdentity → Spree user
     • returns success(user) or failure(message)
              │
              ▼
   Spree issues its own JWT (HS256, iss=spree, aud=store_api)
   + a rotatable RefreshToken
              │
              ▼
Client → subsequent calls with `Authorization: Bearer <Spree 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`).

```ruby app/models/my_app/auth/external_jwt_strategy.rb theme={"theme":"night-owl"}
module MyApp
  module Auth
    class ExternalJwtStrategy < Spree::Authentication::Strategies::BaseStrategy
      PROVIDER = 'external_idp'.freeze

      def provider
        PROVIDER
      end

      def authenticate
        token = params[:token] || extract_bearer
        return failure(I18n.t('spree.api.unauthorized')) if token.blank?

        payload = verify_with_jwks(token)

        user = find_or_create_user_from_oauth(
          provider: PROVIDER,
          uid:      payload.fetch('sub'),
          info:     {
            email:      payload['email'],
            first_name: payload['given_name'],
            last_name:  payload['family_name']
          }
        )

        success(user)
      rescue JWT::DecodeError, JWT::ExpiredSignature, JWT::InvalidIssuerError, JWT::InvalidAudError, KeyError => e
        failure(e.message)
      end

      private

      def verify_with_jwks(token)
        jwks_loader = ->(opts) { jwks(force: opts[:invalidate]) }

        JWT.decode(
          token, nil, true,
          algorithms: ['RS256'],
          iss:        ENV.fetch('EXTERNAL_IDP_ISSUER'),
          aud:        ENV.fetch('EXTERNAL_IDP_AUDIENCE'),
          verify_iss: true,
          verify_aud: true,
          jwks:       jwks_loader
        ).first
      end

      def jwks(force: false)
        Rails.cache.fetch('external_idp:jwks', expires_in: 1.hour, force: force) do
          uri = URI(ENV.fetch('EXTERNAL_IDP_JWKS_URL'))
          JSON.parse(Net::HTTP.get(uri))
        end
      end

      def extract_bearer
        header = request_env['HTTP_AUTHORIZATION'].to_s
        header.start_with?('Bearer ') ? header.split(' ', 2).last : nil
      end
    end
  end
end
```

### 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 to `Spree.store_authentication_strategies` in an initializer. The key you choose here is what clients will send as `provider` in the login payload.

```ruby config/initializers/spree.rb theme={"theme":"night-owl"}
Rails.application.config.after_initialize do
  Spree.store_authentication_strategies.add(:external_idp, MyApp::Auth::ExternalJwtStrategy)
end
```

`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.                                                                                                     |

<Warning>
  `Spree::UserIdentity` validates that `provider` is a registered strategy key. Registration must happen during boot — before the first login attempt.
</Warning>

For an admin-side equivalent, register under `Spree.admin_authentication_strategies` instead and instantiate your strategy against `Spree.admin_user_class`:

```ruby theme={"theme":"night-owl"}
Spree.admin_authentication_strategies.add(:okta, MyApp::Auth::OktaStrategy)
```

## 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`.

```http theme={"theme":"night-owl"}
POST /api/v3/store/auth/login
X-Spree-API-Key: pk_your_publishable_key
Content-Type: application/json

{
  "provider": "external_idp",
  "token":    "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIs..."
}
```

The response is the standard Spree auth payload:

```json theme={"theme":"night-owl"}
{
  "token":         "<Spree HS256 JWT, aud=store_api>",
  "refresh_token": "rt_xxxxxxxxxxxx",
  "user":          { "id": "user_...", "email": "...", "first_name": "...", "...": "..." }
}
```

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.

From `@spree/sdk` the same call looks like:

```ts theme={"theme":"night-owl"}
import { createClient } from '@spree/sdk'

const client = createClient({ baseUrl: 'https://your-store.com', publishableKey: '<pk>' })

const auth = await client.auth.login({
  provider: 'external_idp',
  token:    thirdPartyJwt,
})
```

`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:

```ruby theme={"theme":"night-owl"}
def authenticate
  payload = verify_with_jwks(params[:token])
  email   = payload.fetch('email')

  user = find_user_by_email(email)

  if user
    user.identities.find_or_create_by!(provider: PROVIDER, uid: payload['sub']) do |identity|
      identity.info = { email: email, name: payload['name'] }
    end
  else
    user = find_or_create_user_from_oauth(
      provider: PROVIDER,
      uid:      payload['sub'],
      info:     { email: email, first_name: payload['given_name'], last_name: payload['family_name'] }
    )
  end

  success(user)
end
```

<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.
</Warning>

## Logout

```http theme={"theme":"night-owl"}
POST /api/v3/store/auth/logout
Content-Type: application/json

{ "refresh_token": "rt_xxxxxxxxxxxx" }
```

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 `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.
* **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.
* **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:

```ruby spec/models/my_app/auth/external_jwt_strategy_spec.rb theme={"theme":"night-owl"}
require 'rails_helper'

RSpec.describe MyApp::Auth::ExternalJwtStrategy do
  let(:rsa_private) { OpenSSL::PKey::RSA.generate(2048) }
  let(:jwks)        { { keys: [JWT::JWK.new(rsa_private).export] } }

  let(:token) do
    JWT.encode(
      { sub: 'idp-user-123', email: 'alice@example.com', iss: 'https://idp.example', aud: 'spree' },
      rsa_private, 'RS256'
    )
  end

  before do
    stub_request(:get, ENV['EXTERNAL_IDP_JWKS_URL']).to_return(body: jwks.to_json)
  end

  subject(:result) do
    described_class.new(
      params:      { provider: 'external_idp', token: token },
      request_env: {}
    ).authenticate
  end

  it 'provisions a Spree user on first login' do
    expect { result }.to change(Spree.user_class, :count).by(1)
    expect(result).to be_success
    expect(result.value.email).to eq('alice@example.com')
  end

  it 'reuses the user on subsequent logins' do
    described_class.new(params: { token: token }, request_env: {}).authenticate
    expect { result }.not_to change(Spree.user_class, :count)
  end

  it 'fails on an expired token' do
    expired = JWT.encode({ sub: 'x', exp: 1.hour.ago.to_i, iss: 'https://idp.example', aud: 'spree' }, rsa_private, 'RS256')
    result  = described_class.new(params: { token: expired }, request_env: {}).authenticate
    expect(result).not_to be_success
  end
end
```

## Reference

* `Spree::Authentication::Strategies::BaseStrategy` — `spree/core/app/models/spree/authentication/strategies/base_strategy.rb`
* `Spree::UserIdentity` — `spree/core/app/models/spree/user_identity.rb`
* `Spree::Api::V3::Store::AuthController` — `spree/api/app/controllers/spree/api/v3/store/auth_controller.rb`
* `Spree::Api::V3::JwtAuthentication` — `spree/api/app/controllers/concerns/spree/api/v3/jwt_authentication.rb`
* See also: [Authentication](/developer/customization/authentication) for `Spree.user_class` integration.
