Skip to content

Commit a14c741

Browse files
authored
feat(sdk): add User-Agent header to HTTP and OAuth requests (#66)
* feat(sdk): add User-Agent header to HTTP requests Sets User-Agent and sfdc_user_agent headers on all API requests. SDK uses 'b2c-tooling-sdk/x.x.x', CLI uses 'b2c-cli/x.x.x'. * feat(sdk): add auth middleware for User-Agent on OAuth requests Extends the User-Agent header support to OAuth token requests by creating a separate auth middleware registry. OAuth requests previously bypassed the HTTP middleware chain since they use direct fetch() calls. - Add AuthMiddlewareRegistry for auth-specific middleware - Add b2c:auth-middleware CLI plugin hook - Apply middleware in OAuthStrategy.clientCredentialsGrant() - Register userAgentAuthProvider with the auth middleware registry
1 parent d9520f7 commit a14c741

14 files changed

Lines changed: 1125 additions & 11 deletions

File tree

.changeset/user-agent-header.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@salesforce/b2c-tooling-sdk': minor
3+
---
4+
5+
Add User-Agent header to all HTTP requests. Sets both `User-Agent` and `sfdc_user_agent` headers with the SDK or CLI version (e.g., `b2c-cli/0.1.0` or `b2c-tooling-sdk/0.1.0`).

packages/b2c-tooling-sdk/src/auth/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,12 @@ export {ApiKeyStrategy} from './api-key.js';
8484
// Resolution helpers
8585
export {resolveAuthStrategy, checkAvailableAuthMethods} from './resolve.js';
8686
export type {ResolveAuthStrategyOptions, AvailableAuthMethods} from './resolve.js';
87+
88+
// Auth middleware
89+
export {
90+
globalAuthMiddlewareRegistry,
91+
AuthMiddlewareRegistry,
92+
applyAuthRequestMiddleware,
93+
applyAuthResponseMiddleware,
94+
} from './middleware.js';
95+
export type {AuthMiddleware, AuthMiddlewareProvider} from './middleware.js';
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
/*
2+
* Copyright (c) 2025, Salesforce, Inc.
3+
* SPDX-License-Identifier: Apache-2
4+
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
5+
*/
6+
/**
7+
* Auth Middleware Registry for B2C SDK.
8+
*
9+
* Provides a middleware system specifically for authentication requests (OAuth token requests).
10+
* This is separate from the HTTP middleware registry because auth requests bypass the
11+
* standard openapi-fetch client middleware chain.
12+
*
13+
* ## SDK Usage
14+
*
15+
* ```typescript
16+
* import { globalAuthMiddlewareRegistry, AuthMiddlewareProvider } from '@salesforce/b2c-tooling-sdk/auth';
17+
*
18+
* const loggingProvider: AuthMiddlewareProvider = {
19+
* name: 'auth-logger',
20+
* getMiddleware() {
21+
* return {
22+
* onRequest({ request }) {
23+
* console.log(`[Auth] ${request.method} ${request.url}`);
24+
* return request;
25+
* },
26+
* onResponse({ response }) {
27+
* console.log(`[Auth] ${response.status}`);
28+
* return response;
29+
* },
30+
* };
31+
* },
32+
* };
33+
*
34+
* globalAuthMiddlewareRegistry.register(loggingProvider);
35+
* ```
36+
*
37+
* ## CLI Plugin Usage
38+
*
39+
* Plugins can provide auth middleware via the `b2c:auth-middleware` hook.
40+
*
41+
* @module auth/middleware
42+
*/
43+
44+
/**
45+
* Middleware interface for authentication requests.
46+
*
47+
* Similar to openapi-fetch's Middleware interface, but simplified for auth requests.
48+
*/
49+
export interface AuthMiddleware {
50+
/**
51+
* Called before the auth request is sent.
52+
* Can modify the request or return a new one.
53+
*
54+
* @param params - Object containing the request
55+
* @returns Modified request, or void to use original
56+
*/
57+
onRequest?(params: {request: Request}): Promise<Request | void>;
58+
59+
/**
60+
* Called after the auth response is received.
61+
* Can modify the response or return a new one.
62+
*
63+
* @param params - Object containing request and response
64+
* @returns Modified response, or void to use original
65+
*/
66+
onResponse?(params: {request: Request; response: Response}): Promise<Response | void>;
67+
}
68+
69+
/**
70+
* Middleware provider that supplies middleware for auth requests.
71+
*
72+
* @example
73+
* ```typescript
74+
* const provider: AuthMiddlewareProvider = {
75+
* name: 'user-agent',
76+
* getMiddleware() {
77+
* return {
78+
* onRequest({ request }) {
79+
* request.headers.set('User-Agent', 'my-app/1.0');
80+
* return request;
81+
* },
82+
* };
83+
* },
84+
* };
85+
* ```
86+
*/
87+
export interface AuthMiddlewareProvider {
88+
/**
89+
* Human-readable name for the provider (used in logging/debugging).
90+
*/
91+
readonly name: string;
92+
93+
/**
94+
* Returns middleware for auth requests.
95+
*
96+
* @returns Middleware to apply, or undefined to skip
97+
*/
98+
getMiddleware(): AuthMiddleware | undefined;
99+
}
100+
101+
/**
102+
* Registry for auth middleware providers.
103+
*
104+
* The registry collects middleware from multiple providers and returns
105+
* them in registration order when requested during OAuth token requests.
106+
*
107+
* ## Usage Modes
108+
*
109+
* **SDK Mode**: Register providers directly via `register()`:
110+
* ```typescript
111+
* globalAuthMiddlewareRegistry.register(myProvider);
112+
* ```
113+
*
114+
* **CLI Mode**: Providers are collected via the `b2c:auth-middleware` hook
115+
* and registered during command initialization.
116+
*/
117+
export class AuthMiddlewareRegistry {
118+
private providers: AuthMiddlewareProvider[] = [];
119+
120+
/**
121+
* Registers a middleware provider.
122+
*
123+
* Providers are called in registration order when middleware is requested.
124+
*
125+
* @param provider - The provider to register
126+
*/
127+
register(provider: AuthMiddlewareProvider): void {
128+
this.providers.push(provider);
129+
}
130+
131+
/**
132+
* Unregisters a middleware provider by name.
133+
*
134+
* @param name - The name of the provider to remove
135+
* @returns true if a provider was removed, false if not found
136+
*/
137+
unregister(name: string): boolean {
138+
const index = this.providers.findIndex((p) => p.name === name);
139+
if (index >= 0) {
140+
this.providers.splice(index, 1);
141+
return true;
142+
}
143+
return false;
144+
}
145+
146+
/**
147+
* Collects middleware from all providers.
148+
*
149+
* @returns Array of middleware in registration order
150+
*/
151+
getMiddleware(): AuthMiddleware[] {
152+
const middleware: AuthMiddleware[] = [];
153+
154+
for (const provider of this.providers) {
155+
const m = provider.getMiddleware();
156+
if (m) {
157+
middleware.push(m);
158+
}
159+
}
160+
161+
return middleware;
162+
}
163+
164+
/**
165+
* Clears all registered providers.
166+
*
167+
* Primarily useful for testing.
168+
*/
169+
clear(): void {
170+
this.providers = [];
171+
}
172+
173+
/**
174+
* Returns the number of registered providers.
175+
*/
176+
get size(): number {
177+
return this.providers.length;
178+
}
179+
180+
/**
181+
* Returns the names of all registered providers.
182+
*/
183+
getProviderNames(): string[] {
184+
return this.providers.map((p) => p.name);
185+
}
186+
}
187+
188+
/**
189+
* Global auth middleware registry instance.
190+
*
191+
* This is the default registry used by OAuth strategies. Register
192+
* middleware providers here to have them applied to token requests.
193+
*
194+
* @example
195+
* ```typescript
196+
* import { globalAuthMiddlewareRegistry } from '@salesforce/b2c-tooling-sdk/auth';
197+
*
198+
* globalAuthMiddlewareRegistry.register({
199+
* name: 'user-agent',
200+
* getMiddleware() {
201+
* return {
202+
* onRequest({ request }) {
203+
* request.headers.set('User-Agent', 'my-app/1.0');
204+
* return request;
205+
* },
206+
* };
207+
* },
208+
* });
209+
* ```
210+
*/
211+
export const globalAuthMiddlewareRegistry = new AuthMiddlewareRegistry();
212+
213+
/**
214+
* Applies auth middleware to a request.
215+
*
216+
* This helper applies all registered `onRequest` middleware in order,
217+
* accumulating modifications to the request.
218+
*
219+
* @param request - The original request
220+
* @param middleware - Array of middleware to apply
221+
* @returns The modified request
222+
*/
223+
export async function applyAuthRequestMiddleware(request: Request, middleware: AuthMiddleware[]): Promise<Request> {
224+
let currentRequest = request;
225+
226+
for (const m of middleware) {
227+
if (m.onRequest) {
228+
const result = await m.onRequest({request: currentRequest});
229+
if (result) {
230+
currentRequest = result;
231+
}
232+
}
233+
}
234+
235+
return currentRequest;
236+
}
237+
238+
/**
239+
* Applies auth middleware to a response.
240+
*
241+
* This helper applies all registered `onResponse` middleware in order,
242+
* accumulating modifications to the response.
243+
*
244+
* @param request - The original request (for context)
245+
* @param response - The response to process
246+
* @param middleware - Array of middleware to apply
247+
* @returns The modified response
248+
*/
249+
export async function applyAuthResponseMiddleware(
250+
request: Request,
251+
response: Response,
252+
middleware: AuthMiddleware[],
253+
): Promise<Response> {
254+
let currentResponse = response;
255+
256+
for (const m of middleware) {
257+
if (m.onResponse) {
258+
const result = await m.onResponse({request, response: currentResponse});
259+
if (result) {
260+
currentResponse = result;
261+
}
262+
}
263+
}
264+
265+
return currentResponse;
266+
}

packages/b2c-tooling-sdk/src/auth/oauth.ts

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import type {AuthStrategy, AccessTokenResponse, DecodedJWT} from './types.js';
77
import {getLogger} from '../logging/logger.js';
88
import {DEFAULT_ACCOUNT_MANAGER_HOST} from '../defaults.js';
9+
import {globalAuthMiddlewareRegistry, applyAuthRequestMiddleware, applyAuthResponseMiddleware} from './middleware.js';
910

1011
// Module-level token cache to support multiple instances with same clientId
1112
const ACCESS_TOKEN_CACHE: Map<string, AccessTokenResponse> = new Map();
@@ -165,10 +166,26 @@ export class OAuthStrategy implements AuthStrategy {
165166
}
166167

167168
const credentials = Buffer.from(`${this.config.clientId}:${this.config.clientSecret}`).toString('base64');
168-
const requestHeaders = {
169-
Authorization: `Basic ${credentials}`,
170-
'Content-Type': 'application/x-www-form-urlencoded',
171-
};
169+
170+
// Build request object for middleware
171+
let request = new Request(url, {
172+
method,
173+
headers: {
174+
Authorization: `Basic ${credentials}`,
175+
'Content-Type': 'application/x-www-form-urlencoded',
176+
},
177+
body: params.toString(),
178+
});
179+
180+
// Apply auth middleware (e.g., User-Agent)
181+
const middleware = globalAuthMiddlewareRegistry.getMiddleware();
182+
request = await applyAuthRequestMiddleware(request, middleware);
183+
184+
// Convert headers to object for logging
185+
const requestHeaders: Record<string, string> = {};
186+
request.headers.forEach((value, key) => {
187+
requestHeaders[key] = value;
188+
});
172189

173190
logger.debug(
174191
{clientId: this.config.clientId},
@@ -181,11 +198,11 @@ export class OAuthStrategy implements AuthStrategy {
181198
logger.trace({method, url, headers: requestHeaders, body: params.toString()}, `[Auth REQ BODY] ${method} ${url}`);
182199

183200
const startTime = Date.now();
184-
const response = await fetch(url, {
185-
method,
186-
headers: requestHeaders,
187-
body: params.toString(),
188-
});
201+
let response = await fetch(request);
202+
203+
// Apply response middleware
204+
response = await applyAuthResponseMiddleware(request, response, middleware);
205+
189206
const duration = Date.now() - startTime;
190207

191208
// Debug: Log response summary

0 commit comments

Comments
 (0)