@cap-js/ord 1.3.13 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,34 +24,48 @@ npm install @cap-js/ord
24
24
 
25
25
  ### Authentication
26
26
 
27
- To enforce authentication in the ORD Plugin, set the following environment variables:
27
+ The ORD Plugin supports multiple authentication strategies that can be configured through environment variables or `.cdsrc.json`. Authentication types are automatically detected based on the presence of their configuration - no explicit `types` array is needed.
28
28
 
29
- - `ORD_AUTH_TYPE`: Specifies the authentication types.
30
- - `BASIC_AUTH`: Contains credentials for `basic` authentication.
29
+ **Supported Authentication Methods:**
31
30
 
32
- If `ORD_AUTH_TYPE` is not set, the application starts without authentication. This variable accepts `open` and `basic` (UCL-mTLS is also planned).
31
+ - **Open**: No authentication (default when no other auth is configured)
32
+ - **Basic**: HTTP Basic Authentication with bcrypt-hashed passwords
33
+ - **CF mTLS**: Cloud Foundry mutual TLS authentication
33
34
 
34
- > Note: `open` cannot be combined with `basic` or any other (future) authentication types.
35
+ **Multiple Authentication Strategies**: You can configure multiple authentication methods simultaneously (e.g., both `basic` and `cf-mtls`). The plugin implements an Express-like middleware pattern that tries each configured strategy in order until one succeeds.
36
+
37
+ > Note: When any secure authentication method is configured, open authentication is automatically disabled to ensure security. The ORD document will reflect all active authentication strategies.
35
38
 
36
39
  #### Open
37
40
 
38
- The `open` authentication type bypasses authentication checks.
41
+ The `open` authentication type is the default and bypasses authentication checks. It is automatically used when no other authentication is configured.
39
42
 
40
43
  #### Basic Authentication
41
44
 
42
- The server supports Basic Authentication through an environment variable that contains a JSON string mapping usernames to bcrypt-hashed passwords:
45
+ Configure Basic Authentication using environment variables or `.cdsrc.json`:
46
+
47
+ **Option 1: Environment Variable**
43
48
 
44
49
  ```bash
45
- BASIC_AUTH='{"admin":"***"}'
50
+ BASIC_AUTH='{"admin":"$2y$05$..."}'
46
51
  ```
47
52
 
48
- Alternatively, configure authentication in `.cdsrc.json`:
53
+ **Option 2: Configuration File**
54
+
55
+ Add to your `.cdsrc.json`:
49
56
 
50
57
  ```json
51
- "authentication": {
52
- "types": ["basic"],
53
- "credentials": {
54
- "admin": "***"
58
+ {
59
+ "cds": {
60
+ "ord": {
61
+ "authentication": {
62
+ "basic": {
63
+ "credentials": {
64
+ "admin": "$2y$05$..."
65
+ }
66
+ }
67
+ }
68
+ }
55
69
  }
56
70
  }
57
71
  ```
@@ -98,6 +112,77 @@ This will output something like `admin:$2y$05$...` - use only the hash part (sta
98
112
 
99
113
  </details>
100
114
 
115
+ #### CF mTLS Authentication
116
+
117
+ Configure Cloud Foundry mutual TLS authentication in `.cdsrc.json`:
118
+
119
+ ```json
120
+ {
121
+ "cds": {
122
+ "ord": {
123
+ "authentication": {
124
+ "cfMtls": {
125
+ "certs": [
126
+ {
127
+ "issuer": "CN=SAP PKI Certificate Service Client CA,OU=SAP BTP Clients,O=SAP SE,C=DE",
128
+ "subject": "CN=my-service,OU=SAP Cloud Platform Clients,O=SAP SE,C=DE"
129
+ }
130
+ ],
131
+ "rootCaDn": ["CN=SAP Cloud Root CA,O=SAP SE,C=DE"]
132
+ }
133
+ }
134
+ }
135
+ }
136
+ }
137
+ ```
138
+
139
+ Or use the environment variable:
140
+
141
+ ```bash
142
+ CF_MTLS_TRUSTED_CERTS='{"certs":[...],"rootCaDn":[...]}'
143
+ ```
144
+
145
+ #### Multiple Authentication Strategies
146
+
147
+ You can configure multiple authentication methods simultaneously to support different client types. Authentication types are detected automatically based on configuration presence:
148
+
149
+ **Configuration in `.cdsrc.json`:**
150
+
151
+ ```json
152
+ {
153
+ "cds": {
154
+ "ord": {
155
+ "authentication": {
156
+ "basic": {
157
+ "credentials": {
158
+ "admin": "$2y$05$..."
159
+ }
160
+ },
161
+ "cfMtls": {
162
+ "certs": [...],
163
+ "rootCaDn": [...]
164
+ }
165
+ }
166
+ }
167
+ }
168
+ }
169
+ ```
170
+
171
+ **How it works:**
172
+
173
+ - Authentication types are detected based on what you configure (no `types` array needed)
174
+ - The plugin tries each configured authentication strategy in order
175
+ - The first strategy that successfully authenticates the request is used
176
+ - If a request includes Basic auth headers, Basic authentication is attempted
177
+ - If a request includes mTLS certificate headers, CF mTLS authentication is attempted
178
+ - The ORD document automatically includes all configured authentication methods in its `accessStrategies`
179
+
180
+ **Example scenarios:**
181
+
182
+ - **Basic + CF mTLS**: Supports both API clients using Basic auth and services using mTLS certificates
183
+ - **Basic only**: Only clients with valid Basic auth credentials can access
184
+ - **CF mTLS only**: Only clients with trusted certificates can access
185
+
101
186
  ### Usage
102
187
 
103
188
  #### Programmatic API
package/cds-plugin.js CHANGED
@@ -1,15 +1,9 @@
1
1
  const cds = require("@sap/cds");
2
- const { getAuthConfig } = require("./lib/authentication");
3
2
 
4
3
  if (cds.cli.command === "build") {
5
4
  cds.build?.register?.("ord", require("./lib/build"));
6
5
  }
7
6
 
8
- // load auth config before any service is started
9
- cds.on("bootstrap", async () => {
10
- getAuthConfig();
11
- });
12
-
13
7
  function _lazyRegisterCompileTarget() {
14
8
  const ord = require("./lib/index").ord;
15
9
  Object.defineProperty(this, "ord", { ord });
@@ -0,0 +1,172 @@
1
+ const cds = require("@sap/cds");
2
+ const { AUTHENTICATION_TYPE, ORD_ACCESS_STRATEGY } = require("./constants");
3
+ const Logger = require("./logger");
4
+
5
+ /**
6
+ * Mapping from internal authentication types to ORD access strategy values.
7
+ * This is the single source of truth for auth type to ORD document mapping.
8
+ *
9
+ * @private
10
+ */
11
+ const AUTH_TYPE_ORD_ACCESS_STRATEGY_MAP = Object.freeze({
12
+ [AUTHENTICATION_TYPE.Open]: ORD_ACCESS_STRATEGY.Open,
13
+ [AUTHENTICATION_TYPE.Basic]: ORD_ACCESS_STRATEGY.Basic,
14
+ [AUTHENTICATION_TYPE.CfMtls]: ORD_ACCESS_STRATEGY.CfMtls,
15
+ });
16
+
17
+ /**
18
+ * Derives ORD access strategies from authentication configuration.
19
+ * This function is the main entry point for converting auth config to ORD document format.
20
+ *
21
+ * @param {Object} authConfig - Authentication configuration object
22
+ * @param {string[]} authConfig.types - Array of authentication types (from AUTHENTICATION_TYPE)
23
+ * @returns {Array<{type: string}>} Array of access strategy objects for ORD document
24
+ *
25
+ * @example
26
+ * // With Basic auth configured
27
+ * const authConfig = { types: ['basic'] };
28
+ * const strategies = getAccessStrategiesFromAuthConfig(authConfig);
29
+ * // Returns: [{ type: 'basic-auth' }]
30
+ *
31
+ * @example
32
+ * // With multiple auth types
33
+ * const authConfig = { types: ['basic', 'cf-mtls'] };
34
+ * const strategies = getAccessStrategiesFromAuthConfig(authConfig);
35
+ * // Returns: [{ type: 'basic-auth' }, { type: 'sap:cmp-mtls:v1' }]
36
+ */
37
+ function getAccessStrategiesFromAuthConfig(authConfig) {
38
+ if (!authConfig || !Array.isArray(authConfig.types)) {
39
+ Logger.warn("getAccessStrategiesFromAuthConfig:", "Invalid authConfig, defaulting to 'open'");
40
+ return [{ type: ORD_ACCESS_STRATEGY.Open }];
41
+ }
42
+
43
+ const strategies = authConfig.types
44
+ .map((type) => {
45
+ const ordType = AUTH_TYPE_ORD_ACCESS_STRATEGY_MAP[type];
46
+ if (!ordType) {
47
+ Logger.warn("getAccessStrategiesFromAuthConfig:", `Unknown auth type '${type}', skipping`);
48
+ return null;
49
+ }
50
+ return { type: ordType };
51
+ })
52
+ .filter(Boolean); // Remove null entries
53
+
54
+ // If no valid strategies found, default to open
55
+ if (strategies.length === 0) {
56
+ return [{ type: ORD_ACCESS_STRATEGY.Open }];
57
+ }
58
+
59
+ return strategies;
60
+ }
61
+
62
+ /**
63
+ * Checks if access strategies contain any non-open strategies.
64
+ *
65
+ * @param {Array<{type: string}>} accessStrategies - Array of access strategy objects
66
+ * @returns {boolean} True if any non-open strategy is present
67
+ */
68
+ function hasNonOpenStrategies(accessStrategies) {
69
+ if (!Array.isArray(accessStrategies)) {
70
+ return false;
71
+ }
72
+ return accessStrategies.some((s) => s.type !== ORD_ACCESS_STRATEGY.Open);
73
+ }
74
+
75
+ /**
76
+ * Validates that 'open' strategy does not coexist with non-open strategies.
77
+ * According to ORD specification, 'open' should not be mixed with authenticated strategies.
78
+ *
79
+ * @param {Array<{type: string}>} accessStrategies - Array of access strategy objects
80
+ * @throws {Error} If 'open' coexists with non-open strategies
81
+ */
82
+ function ensureNoOpenWhenNonOpenPresent(accessStrategies) {
83
+ if (!Array.isArray(accessStrategies) || accessStrategies.length === 0) {
84
+ return;
85
+ }
86
+
87
+ const hasOpen = accessStrategies.some((s) => s.type === ORD_ACCESS_STRATEGY.Open);
88
+ const hasNonOpen = hasNonOpenStrategies(accessStrategies);
89
+
90
+ if (hasOpen && hasNonOpen) {
91
+ throw new Error(
92
+ "Invalid access strategies: 'open' cannot coexist with authenticated strategies (basic-auth, sap:cmp-mtls:v1)",
93
+ );
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Ensures access strategies are valid and present, with configurable strict mode.
99
+ * In non-strict mode (default), missing/empty strategies fallback to 'open' with error log.
100
+ * In strict mode, missing/empty strategies throw an error.
101
+ *
102
+ * @param {Array<{type: string}>|undefined} accessStrategies - Array of access strategy objects
103
+ * @param {Object} options - Validation options
104
+ * @param {string} [options.resourceName] - Name of the resource (for error messages)
105
+ * @param {boolean} [options.strict] - If true, throw error instead of fallback (default: reads from cds.env.ord.strictAccessStrategies)
106
+ * @returns {Array<{type: string}>} Validated access strategies array
107
+ * @throws {Error} In strict mode, if accessStrategies is missing or empty
108
+ *
109
+ * @example
110
+ * // Non-strict mode (default) - fallback to open
111
+ * const strategies = ensureAccessStrategies(undefined, { resourceName: 'MyAPI' });
112
+ * // Logs error and returns: [{ type: 'open' }]
113
+ *
114
+ * @example
115
+ * // Strict mode - throws error
116
+ * const strategies = ensureAccessStrategies(undefined, {
117
+ * resourceName: 'MyAPI',
118
+ * strict: true
119
+ * });
120
+ * // Throws: Error with message about missing accessStrategies
121
+ */
122
+ function ensureAccessStrategies(accessStrategies, options = {}) {
123
+ const { resourceName = "unknown resource", strict } = options;
124
+
125
+ // Determine strict mode: explicit parameter > config > default false
126
+ const isStrict = strict !== undefined ? strict : cds.env.ord?.strictAccessStrategies === true;
127
+
128
+ if (!Array.isArray(accessStrategies) || accessStrategies.length === 0) {
129
+ const message = `[ORD] accessStrategies missing or empty for resource "${resourceName}"`;
130
+
131
+ if (isStrict) {
132
+ throw new Error(`${message}. Strict mode is enabled.`);
133
+ } else {
134
+ Logger.error("ensureAccessStrategies:", `${message}. Falling back to 'open'.`);
135
+ return [{ type: ORD_ACCESS_STRATEGY.Open }];
136
+ }
137
+ }
138
+
139
+ // Validate no mixing of 'open' with non-open strategies
140
+ ensureNoOpenWhenNonOpenPresent(accessStrategies);
141
+
142
+ return accessStrategies;
143
+ }
144
+
145
+ /**
146
+ * Validates that access strategies array contains only known ORD access strategy types.
147
+ *
148
+ * @param {Array<{type: string}>} accessStrategies - Array of access strategy objects
149
+ * @returns {boolean} True if all strategies are valid
150
+ */
151
+ function isValidAccessStrategies(accessStrategies) {
152
+ if (!Array.isArray(accessStrategies) || accessStrategies.length === 0) {
153
+ return false;
154
+ }
155
+
156
+ const validTypes = Object.values(ORD_ACCESS_STRATEGY);
157
+ return accessStrategies.every((s) => s.type && validTypes.includes(s.type));
158
+ }
159
+
160
+ module.exports = {
161
+ // Main API
162
+ getAccessStrategiesFromAuthConfig,
163
+ ensureAccessStrategies,
164
+
165
+ // Helper functions
166
+ hasNonOpenStrategies,
167
+ ensureNoOpenWhenNonOpenPresent,
168
+ isValidAccessStrategies,
169
+
170
+ // Constants (re-exported for convenience)
171
+ AUTH_TYPE_ORD_ACCESS_STRATEGY_MAP,
172
+ };
@@ -0,0 +1,351 @@
1
+ /**
2
+ * Authentication Middleware Module
3
+ *
4
+ * This module implements an Express-like middleware pattern for handling multiple
5
+ * authentication strategies. It supports:
6
+ *
7
+ * 1. Strategy Registration: Authentication methods are registered as strategies
8
+ * 2. Multiple Authentication: Basic, CF mTLS, and other methods can coexist
9
+ * 3. Request Routing: Automatically detects request type and routes to appropriate strategy
10
+ * 4. Auto-filtering: When non-open strategies exist, open is automatically ignored
11
+ *
12
+ * Architecture:
13
+ * - Each strategy is a function that returns { success, handled, error }
14
+ * - Strategies are tried in order until one succeeds
15
+ * - Similar to Express middleware chain behavior
16
+ *
17
+ * Supported Authentication Types:
18
+ * - open: No authentication (filtered when combined with secure methods)
19
+ * - basic: HTTP Basic Authentication with bcrypt password hashing
20
+ * - cf-mtls: Cloud Foundry mTLS authentication
21
+ *
22
+ * @module lib/auth/authentication
23
+ */
24
+
25
+ const cds = require("@sap/cds");
26
+ const { AUTHENTICATION_TYPE, BASIC_AUTH_HEADER_KEY, AUTH_STRINGS, CF_MTLS_HEADERS } = require("../constants");
27
+ const Logger = require("../logger");
28
+ const { getAccessStrategiesFromAuthConfig } = require("../access-strategies");
29
+ const bcrypt = require("bcryptjs");
30
+ const { createCfMtlsConfig, handleCfMtlsAuthentication } = require("./cf-mtls");
31
+
32
+ /**
33
+ * Compares a plain text password with a hashed password
34
+ * @param {string} password Plain text password to check
35
+ * @param {string} hashedPassword Hashed password to compare against
36
+ * @returns {Promise<boolean>} Promise resolving to true if passwords match, false otherwise
37
+ */
38
+ async function _comparePassword(password, hashedPassword) {
39
+ if (!password || !hashedPassword) {
40
+ throw new Error("Password and hashed password are required");
41
+ }
42
+ return await bcrypt.compare(password, hashedPassword.replace(/^\$2y/, "$2a"));
43
+ }
44
+
45
+ /**
46
+ * Validates if a string is a bcrypt hash
47
+ * @param {string} hash String to validate
48
+ * @returns {boolean} boolean indicating if the string is a bcrypt hash
49
+ */
50
+ function _isBcryptHash(hash) {
51
+ return /^\$2[ayb]\$\d{2}\$[A-Za-z0-9./]{53}$/.test(hash);
52
+ }
53
+
54
+ /**
55
+ * Lazy-loads and initializes the CF mTLS validator.
56
+ * Uses Promise caching to ensure initialization only happens once even with concurrent requests.
57
+ *
58
+ * @param {Object} authConfig - Authentication configuration object
59
+ * @returns {Promise<Function>} The CF mTLS validator function
60
+ * @throws {Error} If CF mTLS initialization fails
61
+ */
62
+ async function ensureCfMtlsValidator(authConfig) {
63
+ // Already initialized
64
+ if (authConfig.cfMtlsValidator) {
65
+ return authConfig.cfMtlsValidator;
66
+ }
67
+
68
+ // Initialization in progress - wait for it
69
+ if (authConfig._cfMtlsInitPromise) {
70
+ await authConfig._cfMtlsInitPromise;
71
+ return authConfig.cfMtlsValidator;
72
+ }
73
+
74
+ // Start initialization
75
+ Logger.info("Initializing CF mTLS validator (lazy loading)...");
76
+
77
+ authConfig._cfMtlsInitPromise = (async () => {
78
+ try {
79
+ const cfMtlsConfig = await createCfMtlsConfig(cds, Logger);
80
+
81
+ if (cfMtlsConfig.error) {
82
+ throw new Error(cfMtlsConfig.error);
83
+ }
84
+
85
+ authConfig.cfMtlsValidator = cfMtlsConfig.cfMtlsValidator;
86
+ Logger.info("CF mTLS validator initialized successfully");
87
+ } catch (error) {
88
+ // Clean up on failure so retry is possible
89
+ authConfig._cfMtlsInitPromise = null;
90
+ throw error;
91
+ }
92
+ })();
93
+
94
+ await authConfig._cfMtlsInitPromise;
95
+ return authConfig.cfMtlsValidator;
96
+ }
97
+
98
+ /**
99
+ * Create authentication configuration based on environment variables or .cdsrc.json settings.
100
+ *
101
+ * Configuration Priority (highest to lowest):
102
+ * 1. Environment variables (BASIC_AUTH, CF_MTLS_TRUSTED_CERTS) - for production deployments
103
+ * 2. .cdsrc.json settings (cds.env.ord.authentication.basic, cds.env.ord.authentication.cfMtls) - for development and testing
104
+ *
105
+ * Authentication types are automatically detected based on the presence of configuration:
106
+ * - If cds.env.ord.authentication.basic exists → Basic authentication enabled
107
+ * - If cds.env.ord.authentication.cfMtls exists → CF mTLS authentication enabled
108
+ * - Multiple authentication types can coexist and are tried in order
109
+ * - Open authentication is the default when no secure authentication is configured
110
+ *
111
+ * This approach follows the 12-Factor App principles where environment variables
112
+ * can override configuration files for deployment flexibility.
113
+ *
114
+ * Note: CF mTLS validator is lazily initialized on first use to avoid blocking
115
+ * service startup for users not using mTLS authentication.
116
+ *
117
+ * @returns {Object} Authentication configuration object or default configuration object as a fallback.
118
+ */
119
+ function createAuthConfig() {
120
+ const defaultAuthConfig = {
121
+ types: [AUTHENTICATION_TYPE.Open],
122
+ accessStrategies: [{ type: AUTHENTICATION_TYPE.Open }],
123
+ };
124
+
125
+ try {
126
+ const authConfig = { types: [] };
127
+ const ordAuth = cds.env.ord?.authentication || {};
128
+
129
+ // Detect Basic authentication by checking for credentials
130
+ if (process.env.BASIC_AUTH || ordAuth.basic) {
131
+ const credentials = process.env.BASIC_AUTH
132
+ ? JSON.parse(process.env.BASIC_AUTH)
133
+ : ordAuth.basic?.credentials;
134
+
135
+ if (!credentials) {
136
+ Logger.error("createAuthConfig:", "Basic auth enabled but no credentials provided");
137
+ return Object.assign(defaultAuthConfig, { error: "Basic auth credentials not provided" });
138
+ }
139
+
140
+ // Check all passwords in credentials map
141
+ for (const [username, password] of Object.entries(credentials)) {
142
+ if (!_isBcryptHash(password)) {
143
+ Logger.error("createAuthConfig:", `Password for user "${username}" must be a bcrypt hash`);
144
+ return Object.assign(defaultAuthConfig, { error: "All passwords must be bcrypt hashes" });
145
+ }
146
+ }
147
+
148
+ authConfig.types.push(AUTHENTICATION_TYPE.Basic);
149
+ authConfig.credentials = credentials;
150
+ }
151
+
152
+ // Detect CF mTLS authentication by checking for cfMtls config
153
+ if (process.env.CF_MTLS_TRUSTED_CERTS || ordAuth.cfMtls) {
154
+ authConfig.types.push(AUTHENTICATION_TYPE.CfMtls);
155
+ // Mark for lazy initialization - validator will be loaded on first use
156
+ authConfig.cfMtlsValidator = null;
157
+ authConfig._cfMtlsInitPromise = null;
158
+ }
159
+
160
+ // If no authentication types detected, default to Open
161
+ if (authConfig.types.length === 0) {
162
+ Logger.info("createAuthConfig:", 'No authentication configured. Defaulting to "Open" authentication');
163
+ return defaultAuthConfig;
164
+ }
165
+
166
+ // Build accessStrategies for ORD document using centralized mapping logic
167
+ // This ensures consistent mapping between auth types and ORD access strategies
168
+ // All mapping logic is centralized in lib/access-strategies.js
169
+ authConfig.accessStrategies = getAccessStrategiesFromAuthConfig(authConfig);
170
+
171
+ Logger.info("createAuthConfig:", `Configured authentication types: ${authConfig.types.join(", ")}`);
172
+ return authConfig;
173
+ } catch (error) {
174
+ Logger.error("createAuthConfig:", `Configuration error: ${error.message}`);
175
+ return Object.assign(defaultAuthConfig, { error: error.message });
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Authentication strategy handler for Basic authentication
181
+ */
182
+ async function basicAuthStrategy(req, res, authConfig) {
183
+ const authHeader = req.headers[BASIC_AUTH_HEADER_KEY];
184
+
185
+ if (!authHeader) {
186
+ return { success: false, handled: false };
187
+ }
188
+
189
+ // Check if this is a Basic auth request
190
+ if (!authHeader.startsWith(AUTH_STRINGS.BASIC_PREFIX)) {
191
+ // Header exists but not Basic auth - this is an explicit rejection
192
+ return { success: false, handled: true, error: "Invalid authentication type" };
193
+ }
194
+
195
+ try {
196
+ const [username, password] = Buffer.from(authHeader.split(" ")[1], "base64").toString().split(":");
197
+ const credentials = authConfig.credentials;
198
+ const storedPassword = credentials[username];
199
+
200
+ if (storedPassword && (await _comparePassword(password, storedPassword))) {
201
+ return { success: true, handled: true };
202
+ }
203
+
204
+ return { success: false, handled: true, error: "Invalid credentials" };
205
+ } catch (error) {
206
+ Logger.error("Basic auth error:", error.message);
207
+ return { success: false, handled: true, error: "Invalid authentication format" };
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Authentication strategy handler for CF mTLS authentication
213
+ */
214
+ async function cfMtlsAuthStrategy(req, res, authConfig) {
215
+ // Check if request has mTLS indicators
216
+ const hasMtlsHeaders = Object.values(CF_MTLS_HEADERS).some((header) => req.headers[header.toLowerCase()]);
217
+
218
+ if (!hasMtlsHeaders) {
219
+ return { success: false, handled: false };
220
+ }
221
+
222
+ try {
223
+ // Lazy-load validator on first mTLS request
224
+ await ensureCfMtlsValidator(authConfig);
225
+
226
+ // Create a mock res object to capture response sending attempts
227
+ let capturedStatus = null;
228
+ let capturedMessage = null;
229
+ const mockRes = {
230
+ status: (code) => {
231
+ capturedStatus = code;
232
+ return mockRes;
233
+ },
234
+ setHeader: () => mockRes,
235
+ send: (msg) => {
236
+ capturedMessage = msg;
237
+ return mockRes;
238
+ },
239
+ };
240
+
241
+ const result = handleCfMtlsAuthentication(req, mockRes, authConfig, Logger);
242
+
243
+ // If handleCfMtlsAuthentication sent a response, we need to send it
244
+ if (!result.success && capturedStatus && capturedMessage) {
245
+ res.status(capturedStatus).send(capturedMessage);
246
+ return { success: false, handled: true, responseSent: true };
247
+ }
248
+
249
+ return { success: result.success, handled: true };
250
+ } catch (error) {
251
+ Logger.error("CF mTLS initialization failed:", error.message);
252
+ return { success: false, handled: true, error: "Authentication configuration error" };
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Authentication strategy handler for Open authentication
258
+ */
259
+ async function openAuthStrategy() {
260
+ return { success: true, handled: true };
261
+ }
262
+
263
+ /**
264
+ * Strategy registry mapping authentication types to their handlers
265
+ */
266
+ const AUTH_STRATEGIES = {
267
+ [AUTHENTICATION_TYPE.Basic]: basicAuthStrategy,
268
+ [AUTHENTICATION_TYPE.CfMtls]: cfMtlsAuthStrategy,
269
+ [AUTHENTICATION_TYPE.Open]: openAuthStrategy,
270
+ };
271
+
272
+ /**
273
+ * Creates an authentication middleware with the given configuration.
274
+ * This factory function returns a middleware that uses the provided authConfig via closure.
275
+ *
276
+ * @param {Object} authConfig - Authentication configuration object
277
+ * @returns {Function} Express middleware function
278
+ */
279
+ function createAuthMiddleware(authConfig) {
280
+ return async function authenticate(req, res, next) {
281
+ // Handle invalid configuration
282
+ if (!authConfig || !authConfig.types || !Array.isArray(authConfig.types)) {
283
+ Logger.error("Invalid auth configuration:", authConfig);
284
+ return res.status(401).send("Not authorized");
285
+ }
286
+
287
+ // If open authentication, allow immediately
288
+ if (authConfig.types.includes(AUTHENTICATION_TYPE.Open)) {
289
+ res.status(200);
290
+ return next();
291
+ }
292
+
293
+ // Try each registered authentication strategy
294
+ const results = [];
295
+
296
+ for (const authType of authConfig.types) {
297
+ const strategy = AUTH_STRATEGIES[authType];
298
+
299
+ if (!strategy) {
300
+ Logger.warn(`Unknown authentication type: ${authType}`);
301
+ continue;
302
+ }
303
+
304
+ try {
305
+ const result = await strategy(req, res, authConfig);
306
+ results.push({ type: authType, ...result });
307
+
308
+ if (result.success) {
309
+ res.status(200);
310
+ return next();
311
+ }
312
+
313
+ // If the strategy already sent a response (e.g., CF mTLS specific error codes)
314
+ if (result.responseSent) {
315
+ return;
316
+ }
317
+ } catch (error) {
318
+ Logger.error(`Error in ${authType} authentication:`, error.message);
319
+ results.push({ type: authType, success: false, handled: true, error: error.message });
320
+ }
321
+ }
322
+
323
+ // If we reach here, authentication failed
324
+ // Check if any strategy was attempted
325
+ const attemptedStrategies = results.filter((r) => r.handled);
326
+
327
+ if (attemptedStrategies.length === 0) {
328
+ // No authentication method was attempted
329
+ const wwwAuthHeaders = [];
330
+
331
+ if (authConfig.types.includes(AUTHENTICATION_TYPE.Basic)) {
332
+ wwwAuthHeaders.push(AUTH_STRINGS.WWW_AUTHENTICATE_REALM);
333
+ }
334
+
335
+ if (wwwAuthHeaders.length > 0) {
336
+ res.setHeader("WWW-Authenticate", wwwAuthHeaders.join(", "));
337
+ }
338
+
339
+ return res.status(401).send("Authentication required.");
340
+ }
341
+
342
+ // At least one strategy was attempted but failed
343
+ const firstError = attemptedStrategies.find((r) => r.error);
344
+ return res.status(401).send(firstError?.error || "Authentication failed");
345
+ };
346
+ }
347
+
348
+ module.exports = {
349
+ createAuthMiddleware,
350
+ createAuthConfig,
351
+ };