Skip to content

Commit c34103b

Browse files
authored
feat: add two-factor client certificate (mTLS) support for WebDAV (#94)
Add support for client certificates and SSL verification control for WebDAV operations, enabling secure connections to instances that require two-factor authentication. New CLI flags: - --certificate: Path to PKCS12 (.p12/.pfx) certificate file - --passphrase: Passphrase for the certificate - --selfsigned/--no-verify: Disable SSL certificate verification Environment variables: - SFCC_CERTIFICATE, SFCC_CERTIFICATE_PASSPHRASE, SFCC_SELFSIGNED dw.json fields: - certificate, certificate-passphrase, self-signed Also adds 'server' and 'webdav-server' as dw.json field aliases for consistency with CLI flag names.
1 parent 3d87305 commit c34103b

File tree

22 files changed

+496
-21
lines changed

22 files changed

+496
-21
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@salesforce/b2c-tooling-sdk': minor
3+
'@salesforce/b2c-cli': minor
4+
---
5+
6+
Add two-factor client certificate (mTLS) support for WebDAV operations
7+
8+
New CLI flags for instance commands:
9+
- `--certificate <path>`: Path to PKCS12 (.p12/.pfx) certificate file
10+
- `--passphrase <string>`: Passphrase for the certificate
11+
- `--selfsigned`: Disable SSL certificate verification (for self-signed certs)
12+
- `--no-verify`: Alias for --selfsigned
13+
14+
Environment variables: `SFCC_CERTIFICATE`, `SFCC_CERTIFICATE_PASSPHRASE`, `SFCC_SELFSIGNED`
15+
16+
dw.json fields: `certificate`, `certificate-passphrase`, `self-signed`
17+
18+
**SDK Note**: The `AuthStrategy.fetch` method signature changed from `RequestInit` to `FetchInit`. Custom `AuthStrategy` implementations should update their type annotations.

docs/guide/configuration.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ You can configure the CLI using environment variables:
6868
| `SFCC_AUTH_METHODS` | Comma-separated list of allowed auth methods |
6969
| `SFCC_OAUTH_SCOPES` | OAuth scopes to request |
7070
| `SFCC_CODE_VERSION` | Code version for deployments |
71+
| `SFCC_CERTIFICATE` | Path to PKCS12 certificate for two-factor auth (mTLS) |
72+
| `SFCC_CERTIFICATE_PASSPHRASE` | Passphrase for the certificate |
73+
| `SFCC_SELFSIGNED` | Allow self-signed server certificates |
7174

7275
## .env File
7376

@@ -142,7 +145,7 @@ If no instance is specified, the config with `"active": true` is used.
142145
| Field | Description |
143146
|-------|-------------|
144147
| `hostname` | B2C instance hostname |
145-
| `webdav-hostname` | Separate hostname for WebDAV (if different from main hostname). Also accepts `secureHostname` or `secure-server`. |
148+
| `webdav-hostname` | Separate hostname for WebDAV (if different from main hostname). Also accepts `webdav-server`, `secureHostname`, or `secure-server`. |
146149
| `code-version` | Code version for deployments |
147150
| `client-id` | OAuth client ID |
148151
| `client-secret` | OAuth client secret |
@@ -151,6 +154,27 @@ If no instance is specified, the config with `"active": true` is used.
151154
| `oauth-scopes` | OAuth scopes (array of strings) |
152155
| `auth-methods` | Authentication methods in priority order (array of strings) |
153156
| `shortCode` | SCAPI short code. Also accepts `short-code` or `scapi-shortcode`. |
157+
| `certificate` | Path to PKCS12 certificate for two-factor auth (mTLS) |
158+
| `certificate-passphrase` | Passphrase for the certificate. Also accepts `passphrase`. |
159+
| `self-signed` | Allow self-signed server certificates. Also accepts `selfsigned`. |
160+
161+
### Two-Factor Authentication (mTLS)
162+
163+
For instances that require client certificate authentication:
164+
165+
```json
166+
{
167+
"hostname": "cert.staging.example.demandware.net",
168+
"code-version": "version1",
169+
"username": "your-username",
170+
"password": "your-access-key",
171+
"certificate": "/path/to/client-cert.p12",
172+
"certificate-passphrase": "cert-password",
173+
"self-signed": true
174+
}
175+
```
176+
177+
The certificate must be in PKCS12 format (`.p12` or `.pfx`). The `self-signed` option is often needed for staging environments with internal certificates.
154178

155179
::: tip MRT Configuration
156180
Managed Runtime API key is not stored in `dw.json`. It is loaded from `~/.mobify`. You can specify `mrtProject` and `mrtEnvironment` in `dw.json` for project/environment selection.

packages/b2c-tooling-sdk/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,7 @@
293293
"openapi-fetch": "^0.15.0",
294294
"pino": "^10.1.0",
295295
"pino-pretty": "^13.1.2",
296+
"undici": "^7.0.0",
296297
"xml2js": "^0.6.2"
297298
}
298299
}

packages/b2c-tooling-sdk/src/auth/api-key.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* SPDX-License-Identifier: Apache-2
44
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
55
*/
6-
import type {AuthStrategy} from './types.js';
6+
import type {AuthStrategy, FetchInit} from './types.js';
77
import {getLogger} from '../logging/logger.js';
88

99
/**
@@ -40,10 +40,11 @@ export class ApiKeyStrategy implements AuthStrategy {
4040
logger.debug({headerName, keyPreview}, `[Auth] Using API Key authentication (${headerName}): ${keyPreview}`);
4141
}
4242

43-
async fetch(url: string, init: RequestInit = {}): Promise<Response> {
43+
async fetch(url: string, init: FetchInit = {}): Promise<Response> {
4444
const headers = new Headers(init.headers);
4545
headers.set(this.headerName, this.headerValue);
46-
return fetch(url, {...init, headers});
46+
// Pass through dispatcher for TLS/mTLS support
47+
return fetch(url, {...init, headers} as RequestInit);
4748
}
4849

4950
/**

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* SPDX-License-Identifier: Apache-2
44
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
55
*/
6-
import type {AuthStrategy} from './types.js';
6+
import type {AuthStrategy, FetchInit} from './types.js';
77
import {getLogger} from '../logging/logger.js';
88

99
export class BasicAuthStrategy implements AuthStrategy {
@@ -16,10 +16,12 @@ export class BasicAuthStrategy implements AuthStrategy {
1616
logger.debug({username: user}, `[Auth] Using Basic authentication for user: ${user}`);
1717
}
1818

19-
async fetch(url: string, init: RequestInit = {}): Promise<Response> {
19+
async fetch(url: string, init: FetchInit = {}): Promise<Response> {
2020
const headers = new Headers(init.headers);
2121
headers.set('Authorization', `Basic ${this.encoded}`);
22-
return fetch(url, {...init, headers});
22+
// Pass through dispatcher for TLS/mTLS support
23+
// Node.js fetch accepts dispatcher as an undocumented option
24+
return fetch(url, {...init, headers} as RequestInit);
2325
}
2426

2527
async getAuthorizationHeader(): Promise<string> {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
// Types
6363
export type {
6464
AuthStrategy,
65+
FetchInit,
6566
AccessTokenResponse,
6667
DecodedJWT,
6768
AuthConfig,

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import {createServer, type Server, type IncomingMessage, type ServerResponse} from 'node:http';
77
import type {Socket} from 'node:net';
88
import {URL} from 'node:url';
9-
import type {AuthStrategy, AccessTokenResponse, DecodedJWT} from './types.js';
9+
import type {AuthStrategy, AccessTokenResponse, DecodedJWT, FetchInit} from './types.js';
1010
import {getLogger} from '../logging/logger.js';
1111
import {decodeJWT} from './oauth.js';
1212
import {DEFAULT_ACCOUNT_MANAGER_HOST} from '../defaults.js';
@@ -113,7 +113,7 @@ export class ImplicitOAuthStrategy implements AuthStrategy {
113113
logger.trace({scopes: this.config.scopes}, '[Auth] Configured scopes');
114114
}
115115

116-
async fetch(url: string, init: RequestInit = {}): Promise<Response> {
116+
async fetch(url: string, init: FetchInit = {}): Promise<Response> {
117117
const logger = getLogger();
118118
const method = init.method || 'GET';
119119

@@ -126,7 +126,8 @@ export class ImplicitOAuthStrategy implements AuthStrategy {
126126
headers.set('x-dw-client-id', this.config.clientId);
127127

128128
const startTime = Date.now();
129-
let res = await fetch(url, {...init, headers});
129+
// Pass through dispatcher for TLS/mTLS support
130+
let res = await fetch(url, {...init, headers} as RequestInit);
130131
const duration = Date.now() - startTime;
131132

132133
logger.debug({method, url, status: res.status, duration}, '[Auth] Response');
@@ -140,7 +141,7 @@ export class ImplicitOAuthStrategy implements AuthStrategy {
140141
headers.set('Authorization', `Bearer ${newToken}`);
141142

142143
const retryStart = Date.now();
143-
res = await fetch(url, {...init, headers});
144+
res = await fetch(url, {...init, headers} as RequestInit);
144145
const retryDuration = Date.now() - retryStart;
145146

146147
logger.debug({method, url, status: res.status, duration: retryDuration}, '[Auth] Retry response');

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* SPDX-License-Identifier: Apache-2
44
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
55
*/
6-
import type {AuthStrategy, AccessTokenResponse, DecodedJWT} from './types.js';
6+
import type {AuthStrategy, AccessTokenResponse, DecodedJWT, FetchInit} from './types.js';
77
import {getLogger} from '../logging/logger.js';
88
import {DEFAULT_ACCOUNT_MANAGER_HOST} from '../defaults.js';
99
import {globalAuthMiddlewareRegistry, applyAuthRequestMiddleware, applyAuthResponseMiddleware} from './middleware.js';
@@ -38,22 +38,24 @@ export class OAuthStrategy implements AuthStrategy {
3838
this.accountManagerHost = config.accountManagerHost || DEFAULT_ACCOUNT_MANAGER_HOST;
3939
}
4040

41-
async fetch(url: string, init: RequestInit = {}): Promise<Response> {
41+
async fetch(url: string, init: FetchInit = {}): Promise<Response> {
4242
const token = await this.getAccessToken();
4343

4444
const headers = new Headers(init.headers);
4545
headers.set('Authorization', `Bearer ${token}`);
4646
headers.set('x-dw-client-id', this.config.clientId);
4747

48-
let res = await fetch(url, {...init, headers});
48+
// Pass through dispatcher for TLS/mTLS support
49+
// Node.js fetch accepts dispatcher as an undocumented option
50+
let res = await fetch(url, {...init, headers} as RequestInit);
4951

5052
// RESILIENCE: If the server says 401, the token might have expired or been revoked.
5153
// We retry exactly once after invalidating the cached token.
5254
if (res.status === 401) {
5355
this.invalidateToken();
5456
const newToken = await this.getAccessToken();
5557
headers.set('Authorization', `Bearer ${newToken}`);
56-
res = await fetch(url, {...init, headers});
58+
res = await fetch(url, {...init, headers} as RequestInit);
5759
}
5860

5961
return res;

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,23 @@
33
* SPDX-License-Identifier: Apache-2
44
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
55
*/
6+
7+
/**
8+
* Extended RequestInit that supports undici dispatcher for TLS/mTLS.
9+
* Uses `unknown` for dispatcher to avoid type conflicts between undici package
10+
* and @types/node/undici-types.
11+
*/
12+
export type FetchInit = Omit<RequestInit, 'dispatcher'> & {
13+
/** undici dispatcher for custom TLS options (mTLS, self-signed certs) */
14+
dispatcher?: unknown;
15+
};
16+
617
export interface AuthStrategy {
718
/**
819
* Performs a fetch request with authentication.
920
* Implementations MUST handle header injection and 401 retries (token refresh) internally.
1021
*/
11-
fetch(url: string, init?: RequestInit): Promise<Response>;
22+
fetch(url: string, init?: FetchInit): Promise<Response>;
1223

1324
/**
1425
* Optional: Helper for legacy clients (like a strict WebDAV lib) that need the raw header.

packages/b2c-tooling-sdk/src/cli/config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ export function extractInstanceFlags(flags: ParsedFlags): Partial<NormalizedConf
9090
codeVersion: flags['code-version'] as string | undefined,
9191
username: flags.username as string | undefined,
9292
password: flags.password as string | undefined,
93+
// TLS/mTLS options
94+
certificate: flags.certificate as string | undefined,
95+
certificatePassphrase: flags.passphrase as string | undefined,
96+
selfSigned: (flags.selfsigned as boolean) || !(flags.verify as boolean),
9397
// Include OAuth flags (instance operations often need OAuth too)
9498
...extractOAuthFlags(flags),
9599
};

0 commit comments

Comments
 (0)