> ## Documentation Index
> Fetch the complete documentation index at: https://docs.stora.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Distributing a public plugin

> Connect operators to Stora from distributed code you don't host (WordPress plugins, browser extensions, on-premise packages).

If you're distributing an integration to many operators through code that runs outside infrastructure you control — for example a WordPress plugin, browser extension, client-side app extension, or packaged on-premise tool — you cannot embed OAuth 2.0 client credentials in that distributed code. This guide compares two patterns: the recommended **broker** pattern, and a lighter **authorization proxy** alternative for cases where you accept that Stora tokens will live in each plugin installation.

## Choose an option

**Use the broker pattern when:**

* Your integration is installed by each operator on infrastructure you don't control (their WordPress site, browser, server, or laptop).
* You need access to operator data beyond simple read-only, public information.
* You want a single place to rotate credentials, revoke individual installations, and monitor usage.

**Use the authorization proxy alternative only when:**

* You need to keep the Stora `client_secret` out of distributed plugin code, but you cannot operate a full broker that stores tokens and proxies every API call.
* Your plugin can protect Stora access and refresh tokens on each operator's infrastructure.
* You accept weaker revocation, observability, and compromise isolation than the broker pattern provides.

**You don't need this pattern** if your integration runs on infrastructure you control and operators authorise it directly — that's a standard [Authorization Code flow](/2025-09/guides/authentication#option-c-oauth-2-0-%E2%80%94-authorization-code).

| Pattern             | Stores Stora `client_secret` | Stores Stora access and refresh tokens | Responsible for token security                |
| ------------------- | ---------------------------- | -------------------------------------- | --------------------------------------------- |
| Broker              | Your broker                  | Your broker                            | Your backend infrastructure                   |
| Authorization proxy | Your proxy                   | Each plugin installation               | Your plugin and the operator's infrastructure |

<Note>
  A hosted Shopify app usually uses the standard Authorization Code flow because the app backend is operated by the app developer and can act as the confidential OAuth client. Do not put Stora credentials or tokens in Shopify theme code, app extensions, or other client-side/distributed code. Route those calls through a backend you host.
</Note>

## Option 1: Broker pattern (recommended)

The broker pattern is the preferred option for public plugins. It keeps Stora tokens and OAuth credentials on infrastructure you control, and gives you one place to revoke installations, monitor usage, and handle refresh-token rotation.

### Architecture

The broker pattern splits responsibility across three actors. Your distributed integration code runs inside an environment you don't control — it holds no Stora credentials. Your broker is a backend service you host — it holds your Stora `client_id` and `client_secret`, stores each operator's access and refresh tokens, and is the only thing Stora sees on the other end of OAuth. Stora issues exactly one confidential OAuth application to you, regardless of how many installations of your integration exist.

### Connection flow at a glance

```mermaid theme={null}
sequenceDiagram
    participant User as Operator<br/>(browser)
    participant Plugin as Plugin<br/>(operator's site)
    participant Broker as Your broker<br/>(backend you host)
    participant Stora as Stora OAuth

    User->>Plugin: Clicks "Connect to Stora"
    Plugin-->>User: 302 → broker /connect
    User->>Broker: GET /connect?install_id=…
    Broker->>Broker: Mint state, store {state → install_id}
    Broker-->>User: 302 → Stora /oauth2/authorize
    User->>Stora: Log in, approve scopes
    Stora-->>User: 302 → broker /callback?code=…&state=…
    User->>Broker: GET /callback?code=…&state=…
    Broker->>Stora: POST /oauth2/token (with client_secret)
    Stora-->>Broker: access_token + refresh_token
    Broker->>Broker: Store tokens keyed by install_id
    Broker-->>User: 302 → plugin success URL with broker_token
    User->>Plugin: GET …?broker_token=…
```

### API call flow at a glance

```mermaid theme={null}
sequenceDiagram
    participant Plugin as Plugin
    participant Broker as Your broker
    participant Stora as Stora API

    Plugin->>Broker: GET /api/orders<br/>Authorization: Bearer &lt;broker_token&gt;
    Broker->>Broker: Look up Stora tokens for install<br/>Refresh if expired
    Broker->>Stora: GET /2025-09/orders<br/>Authorization: Bearer &lt;stora_token&gt;
    Stora-->>Broker: 200 OK (JSON)
    Broker-->>Plugin: 200 OK (JSON)
```

<Tip>
  Stora sees one client (your broker), one redirect URI, one pair of credentials. Operators see your plugin. This is what makes the pattern safe to distribute publicly — the credentials that prove identity to Stora never leave your servers.
</Tip>

### Why the broker doesn't hand Stora tokens to the plugin

It's tempting to skip the broker's own token layer and just forward Stora's `access_token` and `refresh_token` to the plugin. Doing so re-creates the problems the broker exists to avoid:

* **Refresh-token theft persists.** Stora's refresh tokens are long-lived bearer credentials. Once exfiltrated from a plugin's database or backup, they work until revoked.
* **You lose the kill switch.** To cut off a single installation you'd have to revoke Stora tokens — which affects the operator's other integrations and requires reconnection.
* **You lose scope narrowing.** Broker-issued tokens can be narrower than the upstream OAuth grant. Raw Stora tokens cannot.
* **You lose observability.** Direct plugin → Stora calls bypass your broker's logs, metrics, and rate limiting.
* **Refresh rotation gets messy.** Stora rotates refresh tokens on every use; with plugin-held tokens, every rotation has to be pushed back to each installation.
* **The `client_secret` is irrelevant after exfiltration.** Attackers use stolen tokens directly — they don't need to mint new ones.

If you decide to hand Stora tokens to the plugin anyway, use the [authorization proxy alternative](#option-2-authorization-proxy-lighter-alternative) below rather than putting your `client_secret` in the plugin. Encrypt tokens at rest and be clear-eyed about what encryption buys you. See [Alternatives considered](#alternatives-considered) for the full list of mitigations and their limits — the short version is that encryption at rest only meaningfully protects against DB dumps, not against RCE or a malicious sibling plugin.

### Connecting an operator

Before you can make API calls on behalf of an operator, the operator's installation needs to go through the Authorization Code flow once. All three of `client_id`, `client_secret`, and the `redirect_uri` belong to your broker — the plugin never sees them. The flow below shows what your broker implements.

<Note>
  This guide assumes Stora has already issued you a confidential OAuth 2.0 application with a single `redirect_uri` pointing at your broker (e.g. `https://broker.yourcompany.com/stora/callback`). Partner credentials are not self-serve today — we provision them for you during onboarding. See [Building a partner integration](/2025-09/guides/partner-integrations) for how to get set up.
</Note>

<Steps>
  <Step title="Plugin starts the connection">
    The operator clicks "Connect to Stora" in your plugin's UI. The plugin redirects the operator's browser to your broker's `/connect` endpoint, passing whatever you use to identify this installation (site URL, install ID, tenant slug).

    ```
    GET https://broker.yourcompany.com/stora/connect
      ?install_id=shop.example.com
      &plugin_nonce=<install-time shared secret>
    ```

    The `plugin_nonce` is yours to design — it lets the broker trust that this redirect actually came from a real installation of your plugin, not a random browser. See [What your broker is responsible for](#what-your-broker-is-responsible-for) for plugin-to-broker authentication notes.
  </Step>

  <Step title="Broker redirects to Stora">
    The broker mints a fresh `state` value, stores `{state → install_id}` in short-lived storage (Redis, or a DB row with a TTL — a few minutes is enough), and redirects the operator's browser to Stora:

    ```
    HTTP/1.1 302 Found
    Location: https://app.stora.co/oauth2/authorize
      ?client_id=YOUR_CLIENT_ID
      &redirect_uri=https://broker.yourcompany.com/stora/callback
      &response_type=code
      &scope=public.contact:read public.order:read
      &state=<opaque, unguessable>
    ```

    The `state` parameter is not optional for a broker: it prevents CSRF on the callback and lets you correlate the returning code with the right installation.
  </Step>

  <Step title="Operator approves in Stora">
    The operator logs in to their Stora BackOffice (if not already) and approves the requested scopes. Stora redirects back to your broker's callback with a one-time code:

    ```
    GET https://broker.yourcompany.com/stora/callback
      ?code=AUTHORIZATION_CODE
      &state=<echoed back>
    ```
  </Step>

  <Step title="Broker exchanges the code for tokens">
    Verify the `state` matches one you issued recently and recover the `install_id`. Then exchange the code at Stora's token endpoint, authenticating with your `client_secret`:

    ```bash theme={null}
    curl -X POST "https://public-api.stora.co/oauth2/token" \
      -H "content-type: application/x-www-form-urlencoded" \
      -d "grant_type=authorization_code" \
      -d "client_id=YOUR_CLIENT_ID" \
      -d "client_secret=YOUR_CLIENT_SECRET" \
      -d "code=AUTHORIZATION_CODE" \
      -d "redirect_uri=https://broker.yourcompany.com/stora/callback"
    ```

    Response:

    ```json theme={null}
    {
      "access_token": "ACCESS_TOKEN",
      "token_type": "Bearer",
      "expires_in": 7200,
      "scope": "public.contact:read public.order:read",
      "created_at": 1710000000,
      "refresh_token": "REFRESH_TOKEN"
    }
    ```
  </Step>

  <Step title="Broker stores tokens and returns to the plugin">
    Store the `access_token`, `refresh_token`, and `expires_at` in your broker's database, keyed by `install_id`. Issue a **broker-scoped token** (an opaque identifier you mint yourself) back to the plugin and redirect to the plugin's success URL:

    ```
    HTTP/1.1 302 Found
    Location: https://shop.example.com/wp-admin/admin.php?page=your-plugin&broker_token=<opaque>
    ```

    From this point on, the plugin holds only the broker token. Stora's `access_token` and `refresh_token` live on your broker and never leave it.
  </Step>
</Steps>

<Tip>
  The `redirect_uri` Stora sees on every connection is always `https://broker.yourcompany.com/stora/callback` — one URL, regardless of how many operator installations exist. That's the property that makes this pattern safe to distribute publicly.
</Tip>

### Making API calls

Once connected, the plugin makes requests to your broker, the broker translates them into Stora API calls using the stored `access_token`, and returns the response to the plugin. The broker is responsible for refreshing expired tokens silently and for surfacing a "reconnect required" signal when refresh fails.

#### The happy path

The plugin calls your broker. Authenticate the plugin with the broker-scoped token you issued during the connect flow:

```bash theme={null}
curl -X GET "https://broker.yourcompany.com/stora/orders" \
  -H "authorization: Bearer BROKER_TOKEN"
```

The broker looks up the installation, checks the stored `expires_at`, refreshes if needed (see below), and proxies the request to Stora:

```bash theme={null}
curl -X GET "https://public-api.stora.co/2025-09/orders" \
  -H "authorization: Bearer STORA_ACCESS_TOKEN"
```

Return the response to the plugin as-is, or reshape it to match your plugin's data model — your choice.

#### Refreshing tokens

Stora access tokens expire after 2 hours. When the stored `expires_at` is within a small buffer of now (e.g. 5 minutes), refresh before making the call:

```bash theme={null}
curl -X POST "https://public-api.stora.co/oauth2/token" \
  -H "content-type: application/x-www-form-urlencoded" \
  -d "grant_type=refresh_token" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET" \
  -d "refresh_token=STORA_REFRESH_TOKEN"
```

The response contains a new `access_token` **and** a new `refresh_token`. The previous refresh token is revoked immediately. Your broker must atomically update both values, or the next refresh will fail.

<Warning>
  Refresh-token rotation is not optional on Stora's side — every successful refresh invalidates the previous refresh token. If your broker loses track of the latest `refresh_token` (crash between HTTP response and DB write, race between two parallel refresh attempts for the same installation), the installation is bricked until the operator reconnects. Serialise refresh attempts per installation and write the new token before returning the new access token to callers.
</Warning>

#### When refresh fails

A `400` from the Stora token endpoint with `"error": "invalid_grant"` means the grant is no longer valid — usually because the operator disconnected your integration in Stora BackOffice. You cannot recover this without the operator going through the connect flow again.

The broker should: mark the installation's tokens as revoked locally, return a well-defined error to the plugin (e.g. `401 Unauthorized` with a body like `{"error":"stora_reconnect_required"}`), and let the plugin prompt the operator to reconnect.

#### Rate limits

Stora applies rate limits per operator (10 req/s, 60 req/min — see [rate limiting](/2025-09/guides/requests#rate-limiting)). Your broker adds no rate-limit protection by default; a `429` from Stora propagates back to the plugin. Consider short request coalescing (same plugin, same endpoint, same operator, within the same second) if your plugin is chatty.

### Disconnecting and reconnecting

Operators need to offboard cleanly, re-grant access after revocation, and occasionally switch which Stora account they've connected — without uninstalling the plugin. Your plugin's settings UI must expose Disconnect and Reconnect actions, and the broker must back them with the logic below.

#### Disconnect

The plugin's settings UI exposes a Disconnect button. When the operator clicks it, the plugin calls a disconnect endpoint on your broker — authenticated with the broker token — and your broker:

1. Calls Stora's `POST /oauth2/revoke` with the stored access token and your OAuth client credentials.
2. Deletes the stored Stora tokens and the broker token for this installation.
3. Returns `204 No Content` to the plugin.

```bash theme={null}
curl -X POST "https://public-api.stora.co/oauth2/revoke" \
  -H "content-type: application/json" \
  -d '{
    "token": "STORA_ACCESS_TOKEN",
    "grant_type": "client_credentials",
    "client_id": "YOUR_CLIENT_ID",
    "client_secret": "YOUR_CLIENT_SECRET"
  }'
```

Stora stores the access token and refresh token on the same OAuth record, so revoking either string revokes the pair — one call is enough.

<Tip>
  Broker routes such as `/stora/connect`, `/stora/callback`, and `/stora/disconnect` are illustrative throughout this guide. Your broker can expose whatever URLs you prefer; only the upstream Stora calls (`/oauth2/authorize`, `/oauth2/token`, `/oauth2/revoke`) are fixed.
</Tip>

#### Reconnect

The plugin's settings UI also exposes a Reconnect button, which redirects the operator back through your existing connect entry point. The broker overwrites the installation's stored tokens on the next successful callback — no new endpoint required.

Expose Reconnect even while the current tokens are still valid. Operators use it to re-grant expanded scopes when your integration adds a new feature, or to switch to a different Stora account without a reinstall.

#### On plugin uninstall

The integration should fire the same disconnect flow automatically when the operator removes it from their host, using whatever uninstall or deprovisioning hook the platform exposes — for example WordPress's `register_uninstall_hook`, or Shopify's `app/uninstalled` webhook for hosted Shopify apps.

Treat uninstall hooks as a hygiene layer, not a replacement for the explicit Disconnect action. Some platforms don't fire them reliably: a user who deletes a WordPress site wholesale never triggers plugin uninstall hooks, and webhook delivery can fail on any hosted platform. The explicit Disconnect action plus Stora-side revocation remains the source of truth.

### What your broker is responsible for

The broker is a piece of infrastructure you own and operate. At minimum, plan for the following.

#### Plugin-to-broker authentication

Stora authenticates your broker via `client_secret`. Your broker needs its own way to authenticate plugin installations. A common pattern: the plugin generates a long random value at activation time, registers it with the broker (HTTPS call keyed by install URL + admin email), and uses it as a bearer token on subsequent calls. Bind each token to one installation so a token stolen from one site can't be used to impersonate another. HMAC-signing requests with a per-install secret is a stronger alternative.

#### Token storage and rotation

Store `access_token`, `refresh_token`, and `expires_at` per installation. Refresh tokens rotate on every use — the previous one is revoked the instant Stora returns a new one. Write the new `refresh_token` to your database *before* you return the response to whoever triggered the refresh, and serialise refresh attempts per installation so two parallel workers can't race. Encrypt tokens at rest; the column on your broker's database is as sensitive as your Stora `client_secret`.

#### Per-installation kill switch

Because the broker mints its own plugin-facing tokens, you can cut off a single installation without touching Stora: invalidate the broker token locally and the plugin starts receiving your reconnect error on every call. This is the fastest response to a compromised installation — no Stora support ticket, no scope re-negotiation, no impact on other installations.

#### Monitoring and abuse detection

Log every call that passes through the broker with `install_id`, endpoint, and status code. Basic alerts to set up: sustained `429` rates per installation (indicates a runaway plugin), sustained `4xx` rates (indicates a broken installation or probing), sudden growth in call volume from a single installation, and anything that looks like credential-stuffing against your own `/connect` endpoint. Ship these to whatever you already use — you don't need anything Stora-specific.

#### Disconnect and reconnect endpoints

Expose routes the plugin can call to trigger [Disconnect](#disconnect) (broker revokes tokens at Stora and wipes local state) and [Reconnect](#reconnect) (broker redirects back through the connect flow). Wire the same revoke-and-discard path into the plugin's platform-specific uninstall hook as best-effort hygiene.

<Tip>
  Operators never see your broker. To them, your plugin is the integration and Stora is the data source. Everything about the broker — its URL, its storage, its uptime — is your private implementation detail.
</Tip>

## Option 2: Authorization proxy (lighter alternative)

The authorization proxy is a lighter alternative to the broker pattern. It keeps your Stora `client_secret` on infrastructure you control, but it does **not** keep Stora tokens out of the plugin. The plugin installation — for example a WordPress site — stores the operator's Stora `access_token` and `refresh_token`, and calls the proxy whenever it needs to exchange an authorization code or refresh a token.

<Warning>
  Prefer the broker pattern for production integrations that handle customer
  PII, payments, contracts, access control, or other sensitive data. The
  authorization proxy reduces deployment complexity, but every plugin
  installation becomes part of your token trust boundary. You are responsible
  for protecting those tokens on the operator's infrastructure. WordPress
  environments vary widely, and tokens can be exposed through database dumps,
  backups, compromised admin accounts, vulnerable plugins, or server-level
  access. The authorization proxy protects your Stora `client_secret`, but it
  does not protect operator tokens after they are issued. If a refresh token is
  leaked, an attacker can mint access tokens until the grant is revoked, or
  until refresh-token rotation invalidates the stolen token before the attacker
  uses it. Choose the broker pattern if you do not want plugin installations to
  hold Stora tokens.
</Warning>

### How the authorization proxy works

The plugin starts the OAuth connection by sending the operator to the authorization proxy. The proxy validates that the plugin callback URL is HTTPS and allowed for the installation, creates a signed `state` value containing the callback context, and redirects the operator to Stora. After approval, Stora redirects back to the proxy with the authorization code; the proxy verifies the signed `state`, creates a signed short-lived `code_context` bound to that code and installation, and redirects the browser back to the plugin with the code.

The plugin then calls the proxy server-to-server to exchange the code for Stora access and refresh tokens. The server-to-server call must authenticate the plugin installation, for example with an `Authorization` header or HMAC signature. The plugin stores the returned tokens locally, but never calls Stora's token endpoint directly. When the access token expires, the plugin sends the stored refresh token to the proxy; the proxy authenticates to Stora with the partner `client_secret`, performs the refresh, and returns the rotated tokens for the plugin to replace locally.

The `code_context` is not a replacement for the Stora authorization code. It is a short-lived signed value created by the proxy so the proxy can stay stateless. It binds the authorization code to the plugin installation and callback that started the flow. When the plugin later calls `/token`, the proxy verifies the `code_context` before using its `client_secret` to exchange the code with Stora.

You can implement `code_context` as a signed JWT/JWS or any equivalent authenticated token format. The payload should include the authorization code hash, installation identifier, callback URL, and short expiry. Sign it with a proxy-only secret. Do not put the Stora `client_secret` or access/refresh tokens in `code_context`.

```json theme={null}
{
  "code_hash": "sha256:BASE64URL_SHA256_OF_AUTHORIZATION_CODE",
  "install_id": "demo-self-storage.example.com",
  "callback_url": "https://demo-self-storage.example.com/wp-admin/admin.php?page=your-plugin",
  "expires_at": 1710000300
}
```

When `/token` is called, verify the signature, expiry, `install_id`, callback URL, and that `sha256(code)` matches `code_hash` before exchanging the code with Stora.

The diagram below uses WordPress as the example plugin host.

```mermaid theme={null}
sequenceDiagram
    autonumber

    actor Operator as Operator
    participant Browser as Operator browser
    participant WP as WordPress site<br/>public plugin
    participant Proxy as Authorization proxy
    participant StoraAuth as Stora OAuth authorize
    participant StoraToken as Stora OAuth token

    Operator->>WP: Click "Connect to Stora"
    WP->>WP: Create wp_state and remember install_id
    WP-->>Browser: Redirect to proxy /connect<br/>install_id, wp_callback, wp_state

    Browser->>Proxy: GET /connect
    Proxy->>Proxy: Validate wp_callback is HTTPS<br/>and allowed for install_id<br/>Create signed stora_state with install_id, wp_callback, wp_state
    Proxy-->>Browser: 302 to Stora /oauth2/authorize<br/>client_id, redirect_uri=proxy callback,<br/>response_type=code, scope, state=stora_state

    Browser->>StoraAuth: Open authorization page
    Operator->>StoraAuth: Log in and approve scopes
    StoraAuth-->>Browser: 302 to proxy callback<br/>code=AUTHORIZATION_CODE, state=stora_state

    Browser->>Proxy: GET /stora/callback?code&state
    Proxy->>Proxy: Verify signed stora_state<br/>Create signed code_context<br/>bound to code_hash, install_id, expiry
    Proxy-->>Browser: 302 to WordPress callback<br/>code=AUTHORIZATION_CODE,<br/>code_context=SIGNED_CONTEXT,<br/>state=wp_state

    Browser->>WP: GET /wp-admin/...your-plugin?code&code_context&state
    WP->>WP: Verify wp_state
    WP->>Proxy: POST /token<br/>code, code_context, install_id<br/>Authorization: Bearer plugin_secret<br/>or HMAC signature
    Proxy->>Proxy: Validate plugin authentication<br/>Verify code_context matches code and install_id
    Proxy->>StoraToken: POST /oauth2/token<br/>grant_type=authorization_code,<br/>client_id, client_secret,<br/>code=AUTHORIZATION_CODE,<br/>redirect_uri=proxy callback
    StoraToken-->>Proxy: access_token, refresh_token, expires_in
    Proxy-->>WP: access_token, refresh_token, expires_in
    WP->>WP: Store tokens securely for this site
    WP-->>Operator: Show connected state

    Note over WP,Proxy: The client_secret stays only in the proxy.<br/>WordPress receives the Stora authorization code, but never receives the client_secret.
    Note over WP: WordPress stores Stora access and refresh tokens for this site.
    Note over Proxy: The proxy can stay stateless by signing state and code_context instead of storing callback data.
    Note over WP,StoraToken: WordPress never calls Stora's token endpoint directly.<br/>Code exchanges and refreshes go through the proxy because Stora requires the partner client_secret.

    rect rgb(245, 248, 255)
        WP->>Proxy: Later: POST /refresh<br/>refresh_token, install_id<br/>Authorization: Bearer plugin_secret<br/>or HMAC signature
        Proxy->>Proxy: Validate plugin authentication<br/>Read client_secret from proxy secrets
        Proxy->>StoraToken: POST /oauth2/token<br/>grant_type=refresh_token,<br/>client_id, client_secret, refresh_token
        StoraToken-->>Proxy: new access_token, new refresh_token
        Proxy-->>WP: new access_token, new refresh_token, expires_in
        WP->>WP: Replace stored tokens with rotated tokens
    end
```

### Authorization proxy trade-offs

Compared with the broker pattern, the authorization proxy removes the need to store per-installation Stora tokens on your backend. That makes the proxy easier to operate, and it can be close to stateless if you use signed, expiring `state` and `code_context` values.

The trade-off is that Stora tokens now live in the plugin installation. A compromised WordPress database, backup, admin account, server, or sibling plugin can expose the operator's Stora tokens. You also lose the broker's per-installation kill switch, API-level observability, rate limiting, and scope narrowing. Use this pattern only when that risk is acceptable, and document the token-storage responsibility clearly for operators.

## Alternatives considered

You don't need to read this section to build the integration. It exists for partners who want to understand why we recommend the broker pattern over the alternatives the OAuth 2.0 spec technically allows.

<AccordionGroup>
  <Accordion title="Why not a public client with PKCE?">
    PKCE protects authorisation codes from interception during the redirect. It does not protect OAuth tokens after they're issued. For a plugin distributed to many operators, the consequences are:

    * **Tokens live on the operator's infrastructure.** `wp_options` is plaintext; other sensitive values in the same table get exfiltrated together in DB dumps, backups copied to staging, and compromised-plugin incidents.
    * **No client authentication.** A public `client_id` can be copied into any application. PKCE binds a code to a device but proves nothing about *who* the client is. A phishing app reusing your `client_id` presents the real Stora consent screen with your branding.
    * **Refresh tokens are long-lived bearer credentials.** Once out of the operator's database they work until revoked. Rotation shrinks the window; it doesn't close it.
    * **Loose redirect URIs.** Thousands of installations mean either wildcard redirect URIs (vulnerable to subdomain takeover) or Dynamic Client Registration (see below).
    * **No abuse isolation.** Revoking the public `client_id` breaks every installation simultaneously.
    * **No consent phishing protection.** An attacker can complete the flow with your `client_id` since there's no secret to prove identity.

    Public clients plus PKCE are designed for **single-user applications on a user's own device** (RFC 8252 — native mobile and desktop apps). They are not designed for server-resident multi-tenant integrations where every installation stores its own long-lived tokens on infrastructure you don't control. Hosted marketplace apps with developer-operated backends are different: the backend can keep credentials and tokens server-side, which is the same security boundary the broker pattern creates.
  </Accordion>

  <Accordion title="Dynamic Client Registration (RFC 7591)">
    DCR would let each plugin installation register its own `client_id` (still public, still PKCE) at install time. Each site then has its own OAuth client record on Stora's side, which solves the redirect-URI and abuse-isolation problems.

    Stora does not currently support Dynamic Client Registration. If you have a use case that specifically requires it, [get in touch](https://stora.co/contact) — we prioritise based on partner demand. DCR alone doesn't solve the token-storage problem (tokens still live in `wp_options`); it mainly simplifies the consent and redirect-URI side of operating many public-client installations.
  </Accordion>

  <Accordion title="If you insist on sharing Stora tokens with the plugin">
    We understand the broker pattern adds operational overhead. If you decide to let the plugin store Stora access and refresh tokens, use the [authorization proxy](#option-2-authorization-proxy-lighter-alternative) shape above. Do **not** put the Stora `client_secret` in the plugin, and do **not** have the plugin call Stora's token endpoint directly. The plugin should receive the authorization code in its callback, then call your proxy to exchange the code or refresh token. Your proxy authenticates to Stora with the `client_secret` and returns the resulting tokens to the plugin.

    At minimum, do the following:

    * **Keep the `client_secret` proxy-side.** The proxy exists to protect the confidential OAuth client credentials. The plugin should only ever see the authorization code, `code_context`, access token, refresh token, expiry, and the plugin-to-proxy credential you design.
    * **Use signed, expiring `state` and `code_context`.** If you want the proxy to stay stateless, encode the callback URL, installation identifier, and nonce inside a signed `state` value. Validate the callback URL is HTTPS and allowlisted for the installation before redirecting. After Stora redirects back, issue a signed `code_context` bound to the authorization code hash, installation identifier, and short expiry. Reject expired, tampered, or mismatched values.
    * **Authenticate plugin-to-proxy calls.** The `/token` and `/refresh` endpoints still need plugin authentication, such as an `Authorization` header with an install-time shared secret or HMAC-signed requests. Do not pass that secret through browser redirects or query strings. Otherwise anyone with a code or refresh token can ask your proxy to use your `client_secret` for them.
    * **Encrypt tokens at rest.** WordPress core stores `wp_options` values as plaintext. Google Site Kit's [`Data_Encryption`](https://github.com/google/site-kit-wp/blob/main/includes/Core/Storage/Data_Encryption.php) is the de-facto reference implementation — AES-256-CTR with a key derived from `LOGGED_IN_KEY` in `wp-config.php`. Most major plugins (Jetpack, WooCommerce Stripe, Mailchimp for WooCommerce) don't encrypt at all; doing so puts you ahead of the ecosystem default.
    * **Understand what encryption buys you.** It only mitigates stolen SQL backups, misconfigured phpMyAdmin, read-only DB leaks, and compromised noisy neighbours on shared MySQL. It does **not** mitigate a compromised WordPress admin, an RCE on the host, a malicious sibling plugin, or a `wp-config.php` leak — in all those cases the key is on the same box.
    * **Narrow your scopes aggressively.** Request only what the plugin genuinely needs. Every scope you request is a scope an attacker inherits.
    * **Rotate refresh tokens through the proxy and persist the replacement.** Stora rotates refresh tokens on every successful refresh. The plugin must send the current refresh token to the proxy, the proxy must refresh with the `client_secret`, and the plugin must atomically replace both the access token and refresh token it has stored.
    * **Accept that you cannot revoke one installation without revoking the operator's entire OAuth grant.** Without a broker-issued token layer, there is no local kill switch granularity — you either revoke the Stora grant or you don't.

    Even with all of the above, the integration's security posture is bounded by the weakest plugin installation running it. The authorization proxy protects your `client_secret`; it does not remove the plugin host from the Stora token trust boundary. The broker pattern does, which is why we recommend it for anything touching customer PII, payments, contracts, access control, or other sensitive data.
  </Accordion>
</AccordionGroup>

## Next steps

You now have the OAuth side built. Before going live, also review:

<CardGroup cols={2}>
  <Card title="Partner programme requirements" href="/2025-09/guides/partner-integrations">
    Timeline Events, idempotency, error handling, scope principles.
  </Card>

  <Card title="Rate limiting and backoff" href="/2025-09/guides/requests#rate-limiting">
    Per-operator limits apply. Your broker or plugin must handle 429s, depending
    on the pattern.
  </Card>

  <Card title="Webhooks" href="/2025-09/guides/webhooks">
    React to Stora events instead of polling. Your broker or proxy receives them
    on a single URL.
  </Card>

  <Card title="Authentication reference" href="/2025-09/guides/authentication">
    Full details on OAuth flows, token exchange, and token refresh.
  </Card>
</CardGroup>

Questions about any of the above, or an integration pattern that doesn't fit these options? [Contact us](https://stora.co/contact) before you start building — it's cheaper than rebuilding.
