Skip to content

Commit 956e994

Browse files
docs: update README.md with instructions about customer management
1 parent d235e58 commit 956e994

1 file changed

Lines changed: 128 additions & 5 deletions

File tree

packages/js-auth/README.md

Lines changed: 128 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@ It works everywhere — on your browser, server, or at the edge.
1313
- [Getting started](#getting-started)
1414
- [API credentials](#api-credentials)
1515
- [Storage strategy](#storage-strategy)
16+
- [Customer storage](#customer-storage)
1617
- [Debugging and storage names](#debugging-and-storage-names)
1718
- [Using unstorage](#using-unstorage)
1819
- [Sales channel](#sales-channel)
1920
- [Password-based customer authentication](#password-based-customer-authentication)
2021
- [JWT bearer authentication](#jwt-bearer-authentication)
22+
- [Delegated login from an external identity provider](#delegated-login-from-an-external-identity-provider)
2123
- [Integration](#integration)
2224
- [Other flows](#other-flows)
2325
- [Webapp application with authorization code flow](#webapp-application-with-authorization-code-flow)
@@ -141,7 +143,7 @@ This library provides a robust token caching system out-of-the-box, with support
141143

142144
- **Single storage** — Provides temporary or persistent storage that can survive page reloads. You can implement any storage solution.
143145
- **Composite storage** — Using the `createCompositeStorage` helper, you can combine multiple storage mechanisms (e.g., memory + Redis) to optimize performance and reduce load on the underlying configured storage.
144-
- **Customer storage** (*sales channel only*) — Optional dedicated storage for customer authentication tokens, separate from guest tokens.
146+
- **[Customer storage](#customer-storage)** (*sales channel only*) — Optional dedicated storage for customer authentication tokens, separate from guest tokens.
145147

146148
Here below an example showing a basic setup with an in-memory storage:
147149

@@ -247,6 +249,74 @@ flowchart TB
247249
class ReturnCustomerToken,ReturnGuestToken endState;
248250
```
249251

252+
#### Customer storage
253+
254+
Sales channels deal with two kinds of tokens: the **guest** token, which is not tied to any identity and can be safely shared, and the **customer** token, which is personal. The optional `customerStorage` option lets you store customer tokens separately from guest tokens.
255+
256+
By default, authorizations are stored using a key derived from the client ID and scope:
257+
258+
```
259+
cl_${type}-${clientId}-${scope}
260+
```
261+
262+
This works out-of-the-box in the browser, where the storage (e.g. `localStorage`) is already scoped to a single visitor.
263+
264+
> [!WARNING]
265+
> The default key does **not** include any customer identity. If customer tokens are cached in a storage shared by all visitors (e.g. Redis or an in-memory map on the server) under the default key, one customer's token would be served to **every** visitor.
266+
267+
There are two ways to store customer tokens safely on the server:
268+
269+
1. **Use a per-visitor `customerStorage`.** When the customer storage is scoped to the visitor (e.g. cookies), the default key is perfectly fine, because the storage itself belongs to a single visitor. This pairs naturally with server-side rendered storefronts, where a single guest token can be cached server-side and shared by all visitors:
270+
271+
```ts
272+
const salesChannel = makeSalesChannel(
273+
{
274+
clientId: "<your_client_id>",
275+
scope: "market:code:europe"
276+
},
277+
{
278+
// The guest token carries no identity:
279+
// one token, cached server-side, serves all visitors...
280+
storage: memoryStorage(),
281+
// ...while the customer token is personal
282+
// and lives in a per-visitor storage (e.g. cookies).
283+
customerStorage: cookieStorageAdapter,
284+
},
285+
)
286+
```
287+
288+
2. **Use a shared storage with a custom key.** If you'd rather cache customer tokens in a shared server-side storage too (e.g. Redis), provide a custom `getKey` function that includes a customer or session identifier. Since `getKey` receives only the client ID, scope, and authorization type, the request context must be captured via closure. Create the helper per request, while keeping the storage backend shared:
289+
290+
```ts
291+
import { createCompositeStorage, makeSalesChannel } from "@commercelayer/js-auth"
292+
293+
// Shared, process-level storage (this is where tokens are actually cached).
294+
const sharedStorage = createCompositeStorage({
295+
name: "bff",
296+
storages: [memoryStorage(), redisStorage],
297+
})
298+
299+
// Per-request helper: creating it is cheap (no network calls, no timers).
300+
function salesChannelFor(customerId: string) {
301+
return makeSalesChannel(
302+
{
303+
clientId: "<your_client_id>",
304+
scope: "market:code:europe"
305+
},
306+
{
307+
storage: sharedStorage,
308+
getKey: async ({ clientId, scope }, type) =>
309+
type === "customer"
310+
? `cl_customer-${clientId}-${scope}-${customerId}`
311+
: `cl_guest-${clientId}-${scope}`,
312+
},
313+
)
314+
}
315+
```
316+
317+
> [!NOTE]
318+
> The helpers returned by `makeSalesChannel` and `makeIntegration` are lightweight factories: they hold no token state themselves (tokens live in the configured storage) and perform no I/O at construction time. It's safe to create one per request, as long as the storage backend is shared. The only thing a long-lived instance adds is in-flight deduplication of concurrent `getAuthorization()` calls.
319+
250320
#### Debugging and storage names
251321

252322
You can enable debugging and assign custom names to your storage instances for better visibility into token operations:
@@ -394,6 +464,12 @@ console.log("Customer access token:", customerAuthorization.accessToken)
394464
* This will remove the customer authorization from the storage, and revoke the access token.
395465
*/
396466
await salesChannel.logoutCustomer()
467+
468+
/**
469+
* You can also clear stored authorizations without revoking the tokens,
470+
* e.g. to force a fresh authorization on the next `getAuthorization()` call.
471+
*/
472+
await salesChannel.removeAuthorization("all") // or "customer" / "guest"
397473
```
398474

399475
Customer authentication is supported through two OAuth 2.0 grant types: [password](#password-based-customer-authentication) and [JWT bearer](#jwt-bearer-authentication). Both flows return an `accessToken`, `scope`, and `refreshToken` that can be stored using the `setCustomer` method.
@@ -407,7 +483,7 @@ import { authenticate } from "@commercelayer/js-auth"
407483
408484
const auth = await authenticate("password", {
409485
clientId: "<your_client_id>",
410-
scope: "market:code:europe"
486+
scope: "market:code:europe",
411487
username: "john@example.com",
412488
password: "secret"
413489
})
@@ -424,7 +500,7 @@ import { authenticate } from "@commercelayer/js-auth"
424500
425501
const newToken = await authenticate("refresh_token", {
426502
clientId: "<your_client_id>",
427-
scope: "market:code:europe"
503+
scope: "market:code:europe",
428504
refreshToken: "<your_refresh_token>"
429505
})
430506
```
@@ -440,6 +516,8 @@ Commerce Layer supports OAuth 2.0 [JWT Bearer](https://docs.commercelayer.io/cor
440516
1. Creating a signed JWT assertion containing the customer's claims
441517

442518
```ts
519+
import { createAssertion } from "@commercelayer/js-auth"
520+
443521
const assertion = await createAssertion({
444522
payload: {
445523
"https://commercelayer.io/claims": {
@@ -466,7 +544,7 @@ Commerce Layer supports OAuth 2.0 [JWT Bearer](https://docs.commercelayer.io/cor
466544
const auth = await authenticate("urn:ietf:params:oauth:grant-type:jwt-bearer", {
467545
clientId: "<your_client_id>",
468546
clientSecret: "<your_client_secret>",
469-
scope: "market:code:europe"
547+
scope: "market:code:europe",
470548
assertion
471549
})
472550
@@ -477,6 +555,51 @@ Commerce Layer supports OAuth 2.0 [JWT Bearer](https://docs.commercelayer.io/cor
477555

478556
Both sales channels and webapps can use this JWT bearer flow to implement secure delegated authentication.
479557

558+
#### Delegated login from an external identity provider
559+
560+
When customers authenticate through an external identity provider (e.g. Okta, Auth0), your backend can exchange their identity for a Commerce Layer customer token using the JWT bearer flow, and hand it over to the `salesChannel` helper (since this flow runs server-side, see the [Customer storage](#customer-storage) section for a suitable setup):
561+
562+
```ts
563+
import {
564+
authenticate,
565+
createAssertion,
566+
} from "@commercelayer/js-auth"
567+
568+
// 1. Validate the external identity (e.g. verify the IdP-issued JWT)
569+
// and resolve it to a Commerce Layer customer ID.
570+
const customerId = await resolveCustomerId(idpToken)
571+
572+
// 2. Create the signed assertion and exchange it for a customer token.
573+
// This step requires the client secret, so it must run server-side.
574+
const assertion = await createAssertion({
575+
payload: {
576+
"https://commercelayer.io/claims": {
577+
owner: {
578+
type: "Customer",
579+
id: customerId
580+
},
581+
},
582+
},
583+
})
584+
585+
const customerCredentials = await authenticate("urn:ietf:params:oauth:grant-type:jwt-bearer", {
586+
clientId: "<your_client_id>",
587+
clientSecret: "<your_client_secret>",
588+
scope: "market:code:europe",
589+
assertion,
590+
})
591+
592+
// 3. Hand the customer token over to the `salesChannel` helper
593+
// created with `makeSalesChannel`.
594+
// From now on, `getAuthorization()` returns the customer authorization
595+
// and refreshes it automatically when it expires.
596+
await salesChannel.setCustomer({
597+
accessToken: customerCredentials.accessToken,
598+
scope: customerCredentials.scope,
599+
refreshToken: customerCredentials.refreshToken,
600+
})
601+
```
602+
480603
### Integration
481604

482605
[Integrations](https://docs.commercelayer.io/core/api-credentials#integration) are used to develop backend integrations with any 3rd-party system.
@@ -744,7 +867,7 @@ The method requires a valid access token (the token can be used with Provisionin
744867

745868
## Contributors guide
746869

747-
1. Fork [this repository](https://github.com/BolajiAyodeji/commercelayer-js-auth) (learn how to do this [here](https://help.github.com/articles/fork-a-repo)).
870+
1. Fork [this repository](https://github.com/commercelayer/commercelayer-js-auth) (learn how to do this [here](https://help.github.com/articles/fork-a-repo)).
748871

749872
2. Clone the forked repository like so:
750873

0 commit comments

Comments
 (0)