Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/user-agent-header.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@salesforce/b2c-tooling-sdk': minor
---

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`).
9 changes: 9 additions & 0 deletions packages/b2c-tooling-sdk/src/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,12 @@ export {ApiKeyStrategy} from './api-key.js';
// Resolution helpers
export {resolveAuthStrategy, checkAvailableAuthMethods} from './resolve.js';
export type {ResolveAuthStrategyOptions, AvailableAuthMethods} from './resolve.js';

// Auth middleware
export {
globalAuthMiddlewareRegistry,
AuthMiddlewareRegistry,
applyAuthRequestMiddleware,
applyAuthResponseMiddleware,
} from './middleware.js';
export type {AuthMiddleware, AuthMiddlewareProvider} from './middleware.js';
266 changes: 266 additions & 0 deletions packages/b2c-tooling-sdk/src/auth/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
/*
* Copyright (c) 2025, Salesforce, Inc.
* SPDX-License-Identifier: Apache-2
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
*/
/**
* Auth Middleware Registry for B2C SDK.
*
* Provides a middleware system specifically for authentication requests (OAuth token requests).
* This is separate from the HTTP middleware registry because auth requests bypass the
* standard openapi-fetch client middleware chain.
*
* ## SDK Usage
*
* ```typescript
* import { globalAuthMiddlewareRegistry, AuthMiddlewareProvider } from '@salesforce/b2c-tooling-sdk/auth';
*
* const loggingProvider: AuthMiddlewareProvider = {
* name: 'auth-logger',
* getMiddleware() {
* return {
* onRequest({ request }) {
* console.log(`[Auth] ${request.method} ${request.url}`);
* return request;
* },
* onResponse({ response }) {
* console.log(`[Auth] ${response.status}`);
* return response;
* },
* };
* },
* };
*
* globalAuthMiddlewareRegistry.register(loggingProvider);
* ```
*
* ## CLI Plugin Usage
*
* Plugins can provide auth middleware via the `b2c:auth-middleware` hook.
*
* @module auth/middleware
*/

/**
* Middleware interface for authentication requests.
*
* Similar to openapi-fetch's Middleware interface, but simplified for auth requests.
*/
export interface AuthMiddleware {
/**
* Called before the auth request is sent.
* Can modify the request or return a new one.
*
* @param params - Object containing the request
* @returns Modified request, or void to use original
*/
onRequest?(params: {request: Request}): Promise<Request | void>;

/**
* Called after the auth response is received.
* Can modify the response or return a new one.
*
* @param params - Object containing request and response
* @returns Modified response, or void to use original
*/
onResponse?(params: {request: Request; response: Response}): Promise<Response | void>;
}

/**
* Middleware provider that supplies middleware for auth requests.
*
* @example
* ```typescript
* const provider: AuthMiddlewareProvider = {
* name: 'user-agent',
* getMiddleware() {
* return {
* onRequest({ request }) {
* request.headers.set('User-Agent', 'my-app/1.0');
* return request;
* },
* };
* },
* };
* ```
*/
export interface AuthMiddlewareProvider {
/**
* Human-readable name for the provider (used in logging/debugging).
*/
readonly name: string;

/**
* Returns middleware for auth requests.
*
* @returns Middleware to apply, or undefined to skip
*/
getMiddleware(): AuthMiddleware | undefined;
}

/**
* Registry for auth middleware providers.
*
* The registry collects middleware from multiple providers and returns
* them in registration order when requested during OAuth token requests.
*
* ## Usage Modes
*
* **SDK Mode**: Register providers directly via `register()`:
* ```typescript
* globalAuthMiddlewareRegistry.register(myProvider);
* ```
*
* **CLI Mode**: Providers are collected via the `b2c:auth-middleware` hook
* and registered during command initialization.
*/
export class AuthMiddlewareRegistry {
private providers: AuthMiddlewareProvider[] = [];

/**
* Registers a middleware provider.
*
* Providers are called in registration order when middleware is requested.
*
* @param provider - The provider to register
*/
register(provider: AuthMiddlewareProvider): void {
this.providers.push(provider);
}

/**
* Unregisters a middleware provider by name.
*
* @param name - The name of the provider to remove
* @returns true if a provider was removed, false if not found
*/
unregister(name: string): boolean {
const index = this.providers.findIndex((p) => p.name === name);
if (index >= 0) {
this.providers.splice(index, 1);
return true;
}
return false;
}

/**
* Collects middleware from all providers.
*
* @returns Array of middleware in registration order
*/
getMiddleware(): AuthMiddleware[] {
const middleware: AuthMiddleware[] = [];

for (const provider of this.providers) {
const m = provider.getMiddleware();
if (m) {
middleware.push(m);
}
}

return middleware;
}

/**
* Clears all registered providers.
*
* Primarily useful for testing.
*/
clear(): void {
this.providers = [];
}

/**
* Returns the number of registered providers.
*/
get size(): number {
return this.providers.length;
}

/**
* Returns the names of all registered providers.
*/
getProviderNames(): string[] {
return this.providers.map((p) => p.name);
}
}

/**
* Global auth middleware registry instance.
*
* This is the default registry used by OAuth strategies. Register
* middleware providers here to have them applied to token requests.
*
* @example
* ```typescript
* import { globalAuthMiddlewareRegistry } from '@salesforce/b2c-tooling-sdk/auth';
*
* globalAuthMiddlewareRegistry.register({
* name: 'user-agent',
* getMiddleware() {
* return {
* onRequest({ request }) {
* request.headers.set('User-Agent', 'my-app/1.0');
* return request;
* },
* };
* },
* });
* ```
*/
export const globalAuthMiddlewareRegistry = new AuthMiddlewareRegistry();

/**
* Applies auth middleware to a request.
*
* This helper applies all registered `onRequest` middleware in order,
* accumulating modifications to the request.
*
* @param request - The original request
* @param middleware - Array of middleware to apply
* @returns The modified request
*/
export async function applyAuthRequestMiddleware(request: Request, middleware: AuthMiddleware[]): Promise<Request> {
let currentRequest = request;

for (const m of middleware) {
if (m.onRequest) {
const result = await m.onRequest({request: currentRequest});
if (result) {
currentRequest = result;
}
}
}

return currentRequest;
}

/**
* Applies auth middleware to a response.
*
* This helper applies all registered `onResponse` middleware in order,
* accumulating modifications to the response.
*
* @param request - The original request (for context)
* @param response - The response to process
* @param middleware - Array of middleware to apply
* @returns The modified response
*/
export async function applyAuthResponseMiddleware(
request: Request,
response: Response,
middleware: AuthMiddleware[],
): Promise<Response> {
let currentResponse = response;

for (const m of middleware) {
if (m.onResponse) {
const result = await m.onResponse({request, response: currentResponse});
if (result) {
currentResponse = result;
}
}
}

return currentResponse;
}
35 changes: 26 additions & 9 deletions packages/b2c-tooling-sdk/src/auth/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import type {AuthStrategy, AccessTokenResponse, DecodedJWT} from './types.js';
import {getLogger} from '../logging/logger.js';
import {DEFAULT_ACCOUNT_MANAGER_HOST} from '../defaults.js';
import {globalAuthMiddlewareRegistry, applyAuthRequestMiddleware, applyAuthResponseMiddleware} from './middleware.js';

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

const credentials = Buffer.from(`${this.config.clientId}:${this.config.clientSecret}`).toString('base64');
const requestHeaders = {
Authorization: `Basic ${credentials}`,
'Content-Type': 'application/x-www-form-urlencoded',
};

// Build request object for middleware
let request = new Request(url, {
method,
headers: {
Authorization: `Basic ${credentials}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: params.toString(),
});

// Apply auth middleware (e.g., User-Agent)
const middleware = globalAuthMiddlewareRegistry.getMiddleware();
request = await applyAuthRequestMiddleware(request, middleware);

// Convert headers to object for logging
const requestHeaders: Record<string, string> = {};
request.headers.forEach((value, key) => {
requestHeaders[key] = value;
});

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

const startTime = Date.now();
const response = await fetch(url, {
method,
headers: requestHeaders,
body: params.toString(),
});
let response = await fetch(request);

// Apply response middleware
response = await applyAuthResponseMiddleware(request, response, middleware);

const duration = Date.now() - startTime;

// Debug: Log response summary
Expand Down
Loading
Loading