@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 +98 -13
- package/cds-plugin.js +0 -6
- package/lib/access-strategies.js +172 -0
- package/lib/auth/authentication.js +351 -0
- package/lib/auth/cf-mtls.js +516 -0
- package/lib/auth/mtls-endpoint-service.js +141 -0
- package/lib/build.js +6 -8
- package/lib/constants.js +45 -0
- package/lib/defaults.js +14 -10
- package/lib/extendOrdWithCustom.js +39 -7
- package/lib/index.js +8 -5
- package/lib/interopCsn.js +25 -0
- package/lib/logger.js +3 -12
- package/lib/mcpAdapter.js +132 -0
- package/lib/metaData.js +26 -5
- package/lib/ord-service.js +20 -6
- package/lib/ord.js +35 -4
- package/lib/templates.js +128 -16
- package/package.json +9 -6
- package/lib/authentication.js +0 -153
package/README.md
CHANGED
|
@@ -24,34 +24,48 @@ npm install @cap-js/ord
|
|
|
24
24
|
|
|
25
25
|
### Authentication
|
|
26
26
|
|
|
27
|
-
|
|
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
|
-
|
|
30
|
-
- `BASIC_AUTH`: Contains credentials for `basic` authentication.
|
|
29
|
+
**Supported Authentication Methods:**
|
|
31
30
|
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
53
|
+
**Option 2: Configuration File**
|
|
54
|
+
|
|
55
|
+
Add to your `.cdsrc.json`:
|
|
49
56
|
|
|
50
57
|
```json
|
|
51
|
-
|
|
52
|
-
"
|
|
53
|
-
|
|
54
|
-
|
|
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
|
+
};
|