Authentication and Interceptors
This page answers: how do the generated Wow clients send the user's token, and where do {tenantId} and {ownerId} come from?
Wow clients do not authenticate by themselves. Every request goes through a Fetcher, and whatever a request needs besides its path and body — a token, attribution headers, a refresh when the token expires — is added by that Fetcher's interceptors. Configure them once per service, and every client that uses the Fetcher inherits them.
Which Fetcher a client uses
| The client is created with | It sends through |
|---|---|
{ fetcher: someFetcher } | That instance |
{ fetcher: 'orders' } | The Fetcher registered under that name (new NamedFetcher('orders', …)) |
Neither (new CartCommandClient(), cartQueryClientFactory.createSnapshotQueryClient()) | The Fetcher registered as default: the fetcher export of @ahoo-wang/fetcher, which has no base URL, unless the application registers its own |
Generated clients merge what you pass over their defaults, so { fetcher } keeps the bounded-context base path. An application that talks to one Wow service can therefore register that service as the default and create every client without options:
import { NamedFetcher } from '@ahoo-wang/fetcher';
// Registers itself under 'default', replacing Fetcher's own default instance.
export const wowService = new NamedFetcher('default', {
baseURL: 'https://api.example.com',
timeout: 10_000,
});Import this module once, before any client sends a request.
CoSec
CoSec is the authorization framework Wow services use. @ahoo-wang/fetcher-cosec configures a Fetcher for it:
pnpm add @ahoo-wang/fetcher-cosec @ahoo-wang/fetcher-eventbus @ahoo-wang/fetcher-storageimport { Fetcher, NamedFetcher } from '@ahoo-wang/fetcher';
import { CoSecConfigurer, CoSecTokenRefresher } from '@ahoo-wang/fetcher-cosec';
export const wowService = new NamedFetcher('default', {
baseURL: 'https://api.example.com',
});
export const cosec = new CoSecConfigurer({
appId: 'order-console',
// Refresh on a separate Fetcher, without the CoSec interceptors.
tokenRefresher: new CoSecTokenRefresher({
fetcher: new Fetcher({ baseURL: 'https://api.example.com' }),
endpoint: '/auth/refresh',
}),
onUnauthorized: () => window.location.assign('/login'),
onForbidden: async () => console.warn('Access denied'),
});
cosec.applyTo(wowService);After sign-in, store the token pair; from then on every request carries it:
import type { CoSecConfigurer } from '@ahoo-wang/fetcher-cosec';
export function onSignedIn(
cosec: CoSecConfigurer,
token: { accessToken: string; refreshToken: string },
) {
cosec.tokenStorage.signIn(token);
}What applyTo adds to every request of that Fetcher:
| Interceptor | Effect |
|---|---|
| CoSec request headers | CoSec-App-Id, a persistent CoSec-Device-Id, a fresh CoSec-Request-Id |
Authorization (only with a tokenRefresher) | Authorization: Bearer <access token>; refreshes the token before it expires, once for concurrent requests, and retries a request answered 401 after a refresh |
| Resource attribution | Fills the {tenantId} path variable from the token's tenantId claim and {ownerId} from its sub claim, when the path has the variable and the request did not set it |
onUnauthorized, onForbidden | Called on a 401 the refresh could not fix, and on a 403 |
Resource attribution is why generated clients leave tenantId and ownerId out of their method parameters: for a tenant- or owner-scoped aggregate (/tenant/{tenantId}/…, /owner/{ownerId}/…), the signed-in user supplies them. Without CoSec, supply them yourself through urlParams.path, per client or per request:
import { CartCommandClient } from './generated/index.js';
export const commandsFor = (ownerId: string) =>
new CartCommandClient({ urlParams: { path: { ownerId } } });The query side works the same way: cartQueryClientFactory.createSnapshotQueryClient({ urlParams: { path: { ownerId } } }).
Wow's space
A spaced aggregate reads its space from the Wow-Space-Id header, on commands and queries alike. CoSec's own spaceIdProvider sends CoSec-Space-Id, which Wow does not read. Send Wow's header per command with commandHeaders({ spaceId }), or for every request of a client through its headers:
import { WowHeaders } from '@ahoo-wang/wow-client';
import { cartQueryClientFactory } from './generated/index.js';
export const storeSnapshots = (spaceId: string) =>
cartQueryClientFactory.createSnapshotQueryClient({
headers: { [WowHeaders.SPACE_ID]: spaceId },
});Your own interceptor
A request interceptor sees every request before it is sent. Order it before URL resolution (URL_RESOLVE_INTERCEPTOR_ORDER) when it sets a path variable, anywhere before the fetch when it only sets headers:
import {
URL_RESOLVE_INTERCEPTOR_ORDER,
type FetchExchange,
type Fetcher,
type RequestInterceptor,
} from '@ahoo-wang/fetcher';
export class TraceparentInterceptor implements RequestInterceptor {
readonly name = 'TraceparentInterceptor';
readonly order = URL_RESOLVE_INTERCEPTOR_ORDER - 1;
constructor(private readonly traceparent: () => string | undefined) {}
intercept(exchange: FetchExchange) {
const value = this.traceparent();
if (value) exchange.ensureRequestHeaders()['traceparent'] = value;
}
}
export function trace(fetcher: Fetcher, traceparent: () => string | undefined) {
fetcher.interceptors.request.use(new TraceparentInterceptor(traceparent));
}Response and error interceptors work the same way; see Fetcher interceptors. An error interceptor cannot turn a failed call into a WowError — read failures with toWowError, as in Error Handling.
Several Wow services
Give each service its own Fetcher and apply CoSec to each. Register them by name and pass the name, or pass the instance:
import { NamedFetcher } from '@ahoo-wang/fetcher';
import type { CoSecConfigurer } from '@ahoo-wang/fetcher-cosec';
export function registerServices(cosec: CoSecConfigurer) {
for (const [name, baseURL] of [
['orders', 'https://orders.example.com'],
['inventory', 'https://inventory.example.com'],
] as const) {
cosec.applyTo(new NamedFetcher(name, { baseURL }));
}
}
// new OrderCommandClient({ fetcher: 'orders' })One CoSecConfigurer applied to several Fetchers shares one token store, so the user signs in once. Behind a gateway that routes by bounded context, one Fetcher for the gateway is enough: the generated clients' context prefix selects the service.
Where to read more
- CoSec with Fetcher and the fetcher-cosec reference: token storage, refresh and the individual interceptors.
- Identity and resource attribution: the Wow side of tenant, owner and space.
- SSR and Node.js: credentials on a server, where one Fetcher serves many users.