api-ape 3.0.2 → 4.1.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 +59 -572
- package/client/README.md +73 -14
- package/client/auth/crypto/aead.js +214 -0
- package/client/auth/crypto/constants.js +32 -0
- package/client/auth/crypto/encoding.js +104 -0
- package/client/auth/crypto/files.md +27 -0
- package/client/auth/crypto/kdf.js +217 -0
- package/client/auth/crypto-utils.js +118 -0
- package/client/auth/files.md +52 -0
- package/client/auth/key-recovery.js +288 -0
- package/client/auth/recovery/constants.js +37 -0
- package/client/auth/recovery/files.md +23 -0
- package/client/auth/recovery/key-derivation.js +61 -0
- package/client/auth/recovery/sss-browser.js +189 -0
- package/client/auth/share-storage.js +205 -0
- package/client/auth/storage/constants.js +18 -0
- package/client/auth/storage/db.js +132 -0
- package/client/auth/storage/files.md +27 -0
- package/client/auth/storage/keys.js +173 -0
- package/client/auth/storage/shares.js +200 -0
- package/client/browser.js +190 -23
- package/client/connectSocket.js +418 -988
- package/client/connection/README.md +23 -0
- package/client/connection/fileDownload.js +256 -0
- package/client/connection/fileHandling.js +450 -0
- package/client/connection/fileUtils.js +346 -0
- package/client/connection/files.md +71 -0
- package/client/connection/messageHandler.js +105 -0
- package/client/connection/network.js +350 -0
- package/client/connection/proxy.js +233 -0
- package/client/connection/sender.js +333 -0
- package/client/connection/state.js +321 -0
- package/client/connection/subscriptions.js +151 -0
- package/client/files.md +53 -0
- package/client/index.js +298 -142
- package/client/transports/README.md +50 -0
- package/client/transports/files.md +41 -0
- package/client/transports/streamParser.js +195 -0
- package/client/transports/streaming.js +555 -203
- package/dist/ape.js +6 -1
- package/dist/ape.js.map +4 -4
- package/index.d.ts +38 -16
- package/package.json +31 -6
- package/server/README.md +272 -67
- package/server/adapters/README.md +23 -14
- package/server/adapters/files.md +68 -0
- package/server/adapters/firebase.js +543 -160
- package/server/adapters/index.js +362 -112
- package/server/adapters/mongo.js +530 -140
- package/server/adapters/postgres.js +534 -155
- package/server/adapters/redis.js +508 -143
- package/server/adapters/supabase.js +555 -186
- package/server/client/README.md +43 -0
- package/server/client/connection.js +586 -0
- package/server/client/files.md +40 -0
- package/server/client/index.js +342 -0
- package/server/files.md +54 -0
- package/server/index.js +322 -71
- package/server/lib/README.md +26 -0
- package/server/lib/broadcast/clients.js +219 -0
- package/server/lib/broadcast/files.md +58 -0
- package/server/lib/broadcast/index.js +57 -0
- package/server/lib/broadcast/publishProxy.js +110 -0
- package/server/lib/broadcast/pubsub.js +137 -0
- package/server/lib/broadcast/sendProxy.js +103 -0
- package/server/lib/bun.js +315 -99
- package/server/lib/fileTransfer/README.md +63 -0
- package/server/lib/fileTransfer/files.md +30 -0
- package/server/lib/fileTransfer/streaming.js +435 -0
- package/server/lib/fileTransfer.js +710 -326
- package/server/lib/files.md +111 -0
- package/server/lib/httpUtils.js +283 -0
- package/server/lib/loader.js +208 -7
- package/server/lib/longPolling/README.md +63 -0
- package/server/lib/longPolling/files.md +44 -0
- package/server/lib/longPolling/getHandler.js +365 -0
- package/server/lib/longPolling/postHandler.js +327 -0
- package/server/lib/longPolling.js +174 -219
- package/server/lib/main.js +369 -532
- package/server/lib/runtimes/README.md +42 -0
- package/server/lib/runtimes/bun.js +586 -0
- package/server/lib/runtimes/files.md +56 -0
- package/server/lib/runtimes/node.js +511 -0
- package/server/lib/wiring.js +539 -98
- package/server/lib/ws/README.md +35 -0
- package/server/lib/ws/adapters/README.md +54 -0
- package/server/lib/ws/adapters/bun.js +538 -170
- package/server/lib/ws/adapters/deno.js +623 -149
- package/server/lib/ws/adapters/files.md +42 -0
- package/server/lib/ws/files.md +74 -0
- package/server/lib/ws/frames.js +532 -154
- package/server/lib/ws/index.js +207 -10
- package/server/lib/ws/server.js +385 -92
- package/server/lib/ws/socket.js +549 -181
- package/server/lib/wsProvider.js +363 -89
- package/server/plugins/binary.js +282 -0
- package/server/security/README.md +92 -0
- package/server/security/auth/README.md +319 -0
- package/server/security/auth/adapters/files.md +95 -0
- package/server/security/auth/adapters/ldap/constants.js +37 -0
- package/server/security/auth/adapters/ldap/files.md +19 -0
- package/server/security/auth/adapters/ldap/helpers.js +111 -0
- package/server/security/auth/adapters/ldap.js +353 -0
- package/server/security/auth/adapters/oauth2/constants.js +41 -0
- package/server/security/auth/adapters/oauth2/files.md +19 -0
- package/server/security/auth/adapters/oauth2/helpers.js +123 -0
- package/server/security/auth/adapters/oauth2.js +273 -0
- package/server/security/auth/adapters/opaque-handlers.js +314 -0
- package/server/security/auth/adapters/opaque.js +205 -0
- package/server/security/auth/adapters/saml/constants.js +52 -0
- package/server/security/auth/adapters/saml/files.md +19 -0
- package/server/security/auth/adapters/saml/helpers.js +74 -0
- package/server/security/auth/adapters/saml.js +173 -0
- package/server/security/auth/adapters/totp.js +703 -0
- package/server/security/auth/adapters/webauthn.js +625 -0
- package/server/security/auth/files.md +61 -0
- package/server/security/auth/framework/constants.js +27 -0
- package/server/security/auth/framework/files.md +23 -0
- package/server/security/auth/framework/handlers.js +272 -0
- package/server/security/auth/framework/socket-auth.js +177 -0
- package/server/security/auth/handlers/auth-messages.js +143 -0
- package/server/security/auth/handlers/files.md +28 -0
- package/server/security/auth/index.js +290 -0
- package/server/security/auth/mfa/crypto/aead.js +148 -0
- package/server/security/auth/mfa/crypto/constants.js +35 -0
- package/server/security/auth/mfa/crypto/files.md +27 -0
- package/server/security/auth/mfa/crypto/kdf.js +120 -0
- package/server/security/auth/mfa/crypto/utils.js +68 -0
- package/server/security/auth/mfa/crypto-utils.js +80 -0
- package/server/security/auth/mfa/files.md +77 -0
- package/server/security/auth/mfa/ledger/constants.js +75 -0
- package/server/security/auth/mfa/ledger/errors.js +73 -0
- package/server/security/auth/mfa/ledger/files.md +23 -0
- package/server/security/auth/mfa/ledger/share-record.js +32 -0
- package/server/security/auth/mfa/ledger.js +255 -0
- package/server/security/auth/mfa/recovery/constants.js +67 -0
- package/server/security/auth/mfa/recovery/files.md +19 -0
- package/server/security/auth/mfa/recovery/handlers.js +216 -0
- package/server/security/auth/mfa/recovery.js +191 -0
- package/server/security/auth/mfa/sss/constants.js +21 -0
- package/server/security/auth/mfa/sss/files.md +23 -0
- package/server/security/auth/mfa/sss/gf256.js +103 -0
- package/server/security/auth/mfa/sss/serialization.js +82 -0
- package/server/security/auth/mfa/sss.js +161 -0
- package/server/security/auth/mfa/two-of-three/constants.js +58 -0
- package/server/security/auth/mfa/two-of-three/files.md +23 -0
- package/server/security/auth/mfa/two-of-three/handlers.js +241 -0
- package/server/security/auth/mfa/two-of-three/helpers.js +71 -0
- package/server/security/auth/mfa/two-of-three.js +136 -0
- package/server/security/auth/nonce-manager.js +89 -0
- package/server/security/auth/state-machine-mfa.js +269 -0
- package/server/security/auth/state-machine.js +257 -0
- package/server/security/extractRootDomain.js +144 -16
- package/server/security/files.md +51 -0
- package/server/security/origin.js +197 -15
- package/server/security/reply.js +274 -16
- package/server/socket/README.md +119 -0
- package/server/socket/authMiddleware.js +299 -0
- package/server/socket/files.md +86 -0
- package/server/socket/open.js +154 -8
- package/server/socket/pluginHooks.js +334 -0
- package/server/socket/receive.js +184 -224
- package/server/socket/receiveContext.js +117 -0
- package/server/socket/send.js +416 -78
- package/server/socket/tagUtils.js +402 -0
- package/server/utils/README.md +19 -0
- package/server/utils/deepRequire.js +255 -30
- package/server/utils/files.md +57 -0
- package/server/utils/genId.js +182 -20
- package/server/utils/parseUserAgent.js +313 -251
- package/server/utils/userAgent/README.md +65 -0
- package/server/utils/userAgent/files.md +46 -0
- package/server/utils/userAgent/patterns.js +545 -0
- package/utils/README.md +21 -0
- package/utils/files.md +66 -0
- package/utils/jss/README.md +21 -0
- package/utils/jss/decode.js +471 -0
- package/utils/jss/encode.js +312 -0
- package/utils/jss/files.md +68 -0
- package/utils/jss/plugins.js +210 -0
- package/utils/jss.js +219 -273
- package/utils/messageHash.js +238 -35
- package/dist/api-ape.min.js +0 -2
- package/dist/api-ape.min.js.map +0 -7
- package/server/client.js +0 -311
- package/server/lib/broadcast.js +0 -146
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview OPAQUE Authentication Adapter for api-ape Server
|
|
3
|
+
*
|
|
4
|
+
* Implements OPAQUE (Oblivious Pseudo-Random Function based Asymmetric
|
|
5
|
+
* Password-Authenticated Key Exchange) for secure authentication where
|
|
6
|
+
* the server never learns the user's password.
|
|
7
|
+
*
|
|
8
|
+
* ## Protocol Flow (Registration)
|
|
9
|
+
*
|
|
10
|
+
* ```
|
|
11
|
+
* Client Server
|
|
12
|
+
* |-- opaque_reg_start -------->| (user, clientNonce, regRequest)
|
|
13
|
+
* |<- opaque_reg_response ------| (serverNonce, ts, regResponse)
|
|
14
|
+
* |-- opaque_reg_finish ------->| (regRecord)
|
|
15
|
+
* |<- opaque_reg_ok ------------| (success)
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* ## Protocol Flow (Login)
|
|
19
|
+
*
|
|
20
|
+
* ```
|
|
21
|
+
* Client Server
|
|
22
|
+
* |-- opaque_auth_start ------->| (user, clientNonce)
|
|
23
|
+
* |<- opaque_auth_1 ------------| (serverNonce, ts, envelope, oprfResponse)
|
|
24
|
+
* |-- opaque_auth_2 ----------->| (clientAuth)
|
|
25
|
+
* |<- opaque_auth_ok -----------| (assignedPrincipal, serverProof, authMeta)
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* @module server/security/auth/adapters/opaque
|
|
29
|
+
* @see {@link module:server/security/auth/state-machine} for auth state management
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
const crypto = require("crypto");
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* OPAQUE adapter configuration
|
|
36
|
+
* @typedef {Object} OpaqueConfig
|
|
37
|
+
* @property {Function} [getUser] - Async function to fetch user by username
|
|
38
|
+
* @property {Function} [saveUser] - Async function to save user registration
|
|
39
|
+
* @property {Function} [opaqueLib] - OPAQUE library instance (e.g., @cloudflare/opaque)
|
|
40
|
+
* @property {number} [nonceLength=32] - Server nonce length in bytes
|
|
41
|
+
* @property {number} [nonceExpiry=30000] - Nonce expiry in ms
|
|
42
|
+
* @property {string} [serverId] - Server identifier for OPAQUE context
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* OPAQUE message types
|
|
47
|
+
* @enum {string}
|
|
48
|
+
*/
|
|
49
|
+
const OpaqueMessageType = {
|
|
50
|
+
REG_START: "opaque_reg_start",
|
|
51
|
+
REG_RESPONSE: "opaque_reg_response",
|
|
52
|
+
REG_FINISH: "opaque_reg_finish",
|
|
53
|
+
REG_OK: "opaque_reg_ok",
|
|
54
|
+
REG_FAIL: "opaque_reg_fail",
|
|
55
|
+
AUTH_START: "opaque_auth_start",
|
|
56
|
+
AUTH_1: "opaque_auth_1",
|
|
57
|
+
AUTH_2: "opaque_auth_2",
|
|
58
|
+
AUTH_OK: "opaque_auth_ok",
|
|
59
|
+
AUTH_FAIL: "opaque_auth_fail",
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* OPAQUE error codes
|
|
64
|
+
* @enum {string}
|
|
65
|
+
*/
|
|
66
|
+
const OpaqueError = {
|
|
67
|
+
USER_NOT_FOUND: "USER_NOT_FOUND",
|
|
68
|
+
USER_EXISTS: "USER_EXISTS",
|
|
69
|
+
INVALID_PROOF: "INVALID_PROOF",
|
|
70
|
+
INVALID_STATE: "INVALID_STATE",
|
|
71
|
+
NONCE_EXPIRED: "NONCE_EXPIRED",
|
|
72
|
+
NONCE_MISMATCH: "NONCE_MISMATCH",
|
|
73
|
+
MISSING_LIB: "MISSING_LIB",
|
|
74
|
+
INVALID_MESSAGE: "INVALID_MESSAGE",
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/** @private */
|
|
78
|
+
const _defaultUserStore = new Map();
|
|
79
|
+
|
|
80
|
+
/** @private */
|
|
81
|
+
const defaultStorage = {
|
|
82
|
+
/**
|
|
83
|
+
* @param {string} username
|
|
84
|
+
* @returns {Promise<Object|null>}
|
|
85
|
+
*/
|
|
86
|
+
async getUser(username) {
|
|
87
|
+
return _defaultUserStore.get(username) || null;
|
|
88
|
+
},
|
|
89
|
+
/**
|
|
90
|
+
* @param {string} username
|
|
91
|
+
* @param {Object} userData
|
|
92
|
+
* @returns {Promise<boolean>}
|
|
93
|
+
*/
|
|
94
|
+
async saveUser(username, userData) {
|
|
95
|
+
_defaultUserStore.set(username, userData);
|
|
96
|
+
return true;
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Create an OPAQUE adapter instance
|
|
102
|
+
*
|
|
103
|
+
* @param {OpaqueConfig} [config={}] - Configuration options
|
|
104
|
+
* @returns {Object} OPAQUE adapter with registration and authentication methods
|
|
105
|
+
*/
|
|
106
|
+
function createOpaqueAdapter(config = {}) {
|
|
107
|
+
const {
|
|
108
|
+
getUser = defaultStorage.getUser,
|
|
109
|
+
saveUser = defaultStorage.saveUser,
|
|
110
|
+
opaqueLib = null,
|
|
111
|
+
nonceLength = 32,
|
|
112
|
+
nonceExpiry = 30000,
|
|
113
|
+
serverId = "api-ape-opaque-server",
|
|
114
|
+
} = config;
|
|
115
|
+
|
|
116
|
+
const pendingSessions = new Map();
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Generate a session key for tracking pending auth
|
|
120
|
+
* @param {string} clientId - Client identifier
|
|
121
|
+
* @param {string} user - Username
|
|
122
|
+
* @returns {string} Session key
|
|
123
|
+
* @private
|
|
124
|
+
*/
|
|
125
|
+
function sessionKey(clientId, user) {
|
|
126
|
+
return `${clientId}:${user}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Generate a server nonce
|
|
131
|
+
* @returns {Object} Nonce info { nonce, expiresAt }
|
|
132
|
+
* @private
|
|
133
|
+
*/
|
|
134
|
+
function generateNonce() {
|
|
135
|
+
const nonce = crypto.randomBytes(nonceLength).toString("base64url");
|
|
136
|
+
const expiresAt = Date.now() + nonceExpiry;
|
|
137
|
+
return { nonce, expiresAt };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Create canonical binding string for OPAQUE context
|
|
142
|
+
* @param {Object} params - Binding parameters
|
|
143
|
+
* @returns {string} Canonical binding string
|
|
144
|
+
*/
|
|
145
|
+
function createCanonicalBinding({ clientId, clientNonce, serverNonce, user, ts }) {
|
|
146
|
+
return `${clientId}|${clientNonce}|${serverNonce}|${user}|${ts}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const { createOpaqueHandlers } = require("./opaque-handlers");
|
|
150
|
+
const handlers = createOpaqueHandlers({
|
|
151
|
+
getUser,
|
|
152
|
+
saveUser,
|
|
153
|
+
opaqueLib,
|
|
154
|
+
serverId,
|
|
155
|
+
pendingSessions,
|
|
156
|
+
sessionKey,
|
|
157
|
+
generateNonce,
|
|
158
|
+
createCanonicalBinding,
|
|
159
|
+
nonceExpiry,
|
|
160
|
+
OpaqueMessageType,
|
|
161
|
+
OpaqueError,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Clean up pending sessions for a client
|
|
166
|
+
* @param {string} clientId - Client identifier
|
|
167
|
+
*/
|
|
168
|
+
function cleanupClient(clientId) {
|
|
169
|
+
for (const [key] of pendingSessions) {
|
|
170
|
+
if (key.startsWith(clientId + ":")) {
|
|
171
|
+
pendingSessions.delete(key);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Check if the adapter has an OPAQUE library configured
|
|
178
|
+
* @returns {boolean} Whether OPAQUE library is available
|
|
179
|
+
*/
|
|
180
|
+
function hasOpaqueLib() {
|
|
181
|
+
return opaqueLib !== null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
type: "opaque",
|
|
186
|
+
tier: 1,
|
|
187
|
+
MessageType: OpaqueMessageType,
|
|
188
|
+
Error: OpaqueError,
|
|
189
|
+
handleRegStart: handlers.handleRegStart,
|
|
190
|
+
handleRegFinish: handlers.handleRegFinish,
|
|
191
|
+
handleAuthStart: handlers.handleAuthStart,
|
|
192
|
+
handleAuthFinish: handlers.handleAuthFinish,
|
|
193
|
+
cleanupClient,
|
|
194
|
+
hasOpaqueLib,
|
|
195
|
+
createCanonicalBinding,
|
|
196
|
+
_pendingSessions: pendingSessions,
|
|
197
|
+
_defaultUserStore,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
module.exports = {
|
|
202
|
+
createOpaqueAdapter,
|
|
203
|
+
OpaqueMessageType,
|
|
204
|
+
OpaqueError,
|
|
205
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file SAML constants and error codes
|
|
3
|
+
*/
|
|
4
|
+
"use strict";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* SAML message types
|
|
8
|
+
* @enum {string}
|
|
9
|
+
*/
|
|
10
|
+
const SAMLMessageType = {
|
|
11
|
+
AUTH_START: "saml_auth_start",
|
|
12
|
+
AUTH_REDIRECT: "saml_auth_redirect",
|
|
13
|
+
AUTH_CALLBACK: "saml_auth_callback",
|
|
14
|
+
AUTH_OK: "saml_auth_ok",
|
|
15
|
+
AUTH_FAIL: "saml_auth_fail",
|
|
16
|
+
LOGOUT_START: "saml_logout_start",
|
|
17
|
+
LOGOUT_REDIRECT: "saml_logout_redirect",
|
|
18
|
+
LOGOUT_OK: "saml_logout_ok",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* SAML error codes
|
|
23
|
+
* @enum {string}
|
|
24
|
+
*/
|
|
25
|
+
const SAMLError = {
|
|
26
|
+
INVALID_RESPONSE: "INVALID_RESPONSE",
|
|
27
|
+
SIGNATURE_INVALID: "SIGNATURE_INVALID",
|
|
28
|
+
ASSERTION_EXPIRED: "ASSERTION_EXPIRED",
|
|
29
|
+
MISSING_ASSERTION: "MISSING_ASSERTION",
|
|
30
|
+
INVALID_AUDIENCE: "INVALID_AUDIENCE",
|
|
31
|
+
MISSING_NAMEID: "MISSING_NAMEID",
|
|
32
|
+
IDP_ERROR: "IDP_ERROR",
|
|
33
|
+
CONFIG_ERROR: "CONFIG_ERROR",
|
|
34
|
+
SESSION_NOT_FOUND: "SESSION_NOT_FOUND",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Default attribute mapping
|
|
39
|
+
*/
|
|
40
|
+
const DEFAULT_ATTRIBUTE_MAPPING = {
|
|
41
|
+
email: ["email", "urn:oid:0.9.2342.19200300.100.1.3", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"],
|
|
42
|
+
displayName: ["displayName", "cn", "urn:oid:2.16.840.1.113730.3.1.241", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"],
|
|
43
|
+
firstName: ["firstName", "givenName", "urn:oid:2.5.4.42", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname"],
|
|
44
|
+
lastName: ["lastName", "sn", "surname", "urn:oid:2.5.4.4", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname"],
|
|
45
|
+
groups: ["memberOf", "groups", "http://schemas.microsoft.com/ws/2008/06/identity/claims/groups"],
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
module.exports = {
|
|
49
|
+
SAMLMessageType,
|
|
50
|
+
SAMLError,
|
|
51
|
+
DEFAULT_ATTRIBUTE_MAPPING,
|
|
52
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# SAML Adapter Module
|
|
2
|
+
|
|
3
|
+
SAML 2.0 SSO authentication helpers.
|
|
4
|
+
|
|
5
|
+
## Directory Structure
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
saml/
|
|
9
|
+
├── constants.js - SAML message types and error codes
|
|
10
|
+
└── helpers.js - Request storage and ID generation
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Files
|
|
14
|
+
|
|
15
|
+
### `constants.js`
|
|
16
|
+
Defines SAMLMessageType (AUTH_REDIRECT, AUTH_OK, AUTH_FAIL, LOGOUT_*) and SAMLError codes.
|
|
17
|
+
|
|
18
|
+
### `helpers.js`
|
|
19
|
+
Pending request storage and request ID generation for SAML flows.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file SAML helper functions
|
|
3
|
+
*/
|
|
4
|
+
"use strict";
|
|
5
|
+
|
|
6
|
+
const crypto = require("crypto");
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Create per-instance storage for mock mode
|
|
10
|
+
* @returns {Object} Storage adapter with isolated maps
|
|
11
|
+
*/
|
|
12
|
+
function createDefaultStorage() {
|
|
13
|
+
const pendingRequests = new Map();
|
|
14
|
+
const mockUsers = new Map();
|
|
15
|
+
return {
|
|
16
|
+
/**
|
|
17
|
+
* Save a pending SAML request
|
|
18
|
+
* @param {string} requestId - Request ID
|
|
19
|
+
* @param {Object} data - Request data
|
|
20
|
+
* @returns {Promise<boolean>} Success
|
|
21
|
+
*/
|
|
22
|
+
async savePendingRequest(requestId, data) {
|
|
23
|
+
pendingRequests.set(requestId, { ...data, createdAt: Date.now() });
|
|
24
|
+
return true;
|
|
25
|
+
},
|
|
26
|
+
/**
|
|
27
|
+
* Get a pending SAML request
|
|
28
|
+
* @param {string} requestId - Request ID
|
|
29
|
+
* @returns {Promise<Object|null>} Request data or null
|
|
30
|
+
*/
|
|
31
|
+
async getPendingRequest(requestId) {
|
|
32
|
+
return pendingRequests.get(requestId) || null;
|
|
33
|
+
},
|
|
34
|
+
/**
|
|
35
|
+
* Delete a pending SAML request
|
|
36
|
+
* @param {string} requestId - Request ID
|
|
37
|
+
* @returns {Promise<boolean>} Success
|
|
38
|
+
*/
|
|
39
|
+
async deletePendingRequest(requestId) {
|
|
40
|
+
return pendingRequests.delete(requestId);
|
|
41
|
+
},
|
|
42
|
+
/**
|
|
43
|
+
* Register a mock user for testing
|
|
44
|
+
* @param {string} nameId - User name ID
|
|
45
|
+
* @param {Object} attributes - User attributes
|
|
46
|
+
* @returns {Promise<boolean>} Success
|
|
47
|
+
*/
|
|
48
|
+
async registerMockUser(nameId, attributes) {
|
|
49
|
+
mockUsers.set(nameId, attributes);
|
|
50
|
+
return true;
|
|
51
|
+
},
|
|
52
|
+
/**
|
|
53
|
+
* Get a mock user by name ID
|
|
54
|
+
* @param {string} nameId - User name ID
|
|
55
|
+
* @returns {Promise<Object|null>} User attributes or null
|
|
56
|
+
*/
|
|
57
|
+
async getMockUser(nameId) {
|
|
58
|
+
return mockUsers.get(nameId) || null;
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Generate a SAML AuthnRequest ID
|
|
65
|
+
* @returns {string} Request ID
|
|
66
|
+
*/
|
|
67
|
+
function generateRequestId() {
|
|
68
|
+
return "_" + crypto.randomBytes(16).toString("hex");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = {
|
|
72
|
+
createDefaultStorage,
|
|
73
|
+
generateRequestId,
|
|
74
|
+
};
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview SAML Authentication Adapter for api-ape Server
|
|
3
|
+
*
|
|
4
|
+
* Implements SAML 2.0 Single Sign-On (SSO) for enterprise identity providers.
|
|
5
|
+
*
|
|
6
|
+
* @module server/security/auth/adapters/saml
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
"use strict";
|
|
10
|
+
|
|
11
|
+
const { SAMLMessageType, SAMLError } = require("./saml/constants");
|
|
12
|
+
const { createDefaultStorage, generateRequestId } = require("./saml/helpers");
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Create a SAML authentication adapter
|
|
16
|
+
*
|
|
17
|
+
* @param {Object} config - Configuration options
|
|
18
|
+
* @param {Function} verify - Passport.js verify callback
|
|
19
|
+
* @returns {Object} SAML adapter with Passport.js Strategy interface
|
|
20
|
+
*/
|
|
21
|
+
function createSAMLStrategy(config = {}, verify = null) {
|
|
22
|
+
if (typeof config === "function") {
|
|
23
|
+
verify = config;
|
|
24
|
+
config = {};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const storage = createDefaultStorage();
|
|
28
|
+
|
|
29
|
+
const {
|
|
30
|
+
entryPoint = "https://idp.example.com/sso",
|
|
31
|
+
issuer = "api-ape",
|
|
32
|
+
callbackUrl = "http://localhost:3000/auth/saml/callback",
|
|
33
|
+
logoutUrl = null,
|
|
34
|
+
identifierFormat = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
35
|
+
passReqToCallback = false,
|
|
36
|
+
} = config;
|
|
37
|
+
|
|
38
|
+
const strategy = { name: "saml" };
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Handle SAML auth start - generates redirect URL
|
|
42
|
+
* @param {Object} [data={}] - Request data
|
|
43
|
+
* @param {string} [data.relayState=''] - State to preserve across redirect
|
|
44
|
+
* @returns {Promise<Object>} Response with redirect URL
|
|
45
|
+
*/
|
|
46
|
+
async function handleAuthStart(data = {}) {
|
|
47
|
+
const requestId = generateRequestId();
|
|
48
|
+
const { relayState = "" } = data;
|
|
49
|
+
await storage.savePendingRequest(requestId, { relayState, issuer });
|
|
50
|
+
const redirectUrl = `${entryPoint}?SAMLRequest=${encodeURIComponent(requestId)}&RelayState=${encodeURIComponent(relayState)}`;
|
|
51
|
+
return { type: SAMLMessageType.AUTH_REDIRECT, url: redirectUrl, requestId };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Handle SAML callback - processes SAML response
|
|
56
|
+
* @param {Object} data - Callback data
|
|
57
|
+
* @param {string} data.SAMLResponse - SAML response
|
|
58
|
+
* @param {string} [data.RelayState] - Relay state
|
|
59
|
+
* @returns {Promise<Object>} Response with user profile or error
|
|
60
|
+
*/
|
|
61
|
+
async function handleAuthCallback(data) {
|
|
62
|
+
const { SAMLResponse, RelayState } = data;
|
|
63
|
+
if (!SAMLResponse) {
|
|
64
|
+
return { type: SAMLMessageType.AUTH_FAIL, error: SAMLError.MISSING_ASSERTION, message: "No SAML response provided" };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const nameId = SAMLResponse;
|
|
69
|
+
const mockUser = await storage.getMockUser(nameId);
|
|
70
|
+
if (!mockUser) {
|
|
71
|
+
return { type: SAMLMessageType.AUTH_FAIL, error: SAMLError.MISSING_NAMEID, message: "User not found in identity provider" };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const profile = {
|
|
75
|
+
nameID: nameId,
|
|
76
|
+
nameIDFormat: identifierFormat,
|
|
77
|
+
issuer: mockUser.issuer || entryPoint,
|
|
78
|
+
email: mockUser.email || nameId,
|
|
79
|
+
firstName: mockUser.firstName,
|
|
80
|
+
lastName: mockUser.lastName,
|
|
81
|
+
displayName: mockUser.displayName || mockUser.firstName + " " + mockUser.lastName,
|
|
82
|
+
groups: mockUser.groups || [],
|
|
83
|
+
raw: mockUser,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
return { type: SAMLMessageType.AUTH_OK, userId: nameId, profile, relayState: RelayState };
|
|
87
|
+
} catch (err) {
|
|
88
|
+
return { type: SAMLMessageType.AUTH_FAIL, error: SAMLError.INVALID_RESPONSE, message: err.message || "Failed to process SAML response" };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Handle SAML logout start
|
|
94
|
+
* @param {Object} data - Logout data
|
|
95
|
+
* @param {string} data.nameId - User name ID
|
|
96
|
+
* @param {string} [data.sessionIndex] - Session index
|
|
97
|
+
* @returns {Promise<Object>} Response with logout redirect URL
|
|
98
|
+
*/
|
|
99
|
+
async function handleLogoutStart(data) {
|
|
100
|
+
const { nameId, sessionIndex } = data;
|
|
101
|
+
if (!logoutUrl) {
|
|
102
|
+
return { type: SAMLMessageType.LOGOUT_OK, message: "Single logout not configured" };
|
|
103
|
+
}
|
|
104
|
+
const redirectUrl = `${logoutUrl}?NameID=${encodeURIComponent(nameId)}&` + (sessionIndex ? `SessionIndex=${encodeURIComponent(sessionIndex)}` : "");
|
|
105
|
+
return { type: SAMLMessageType.LOGOUT_REDIRECT, url: redirectUrl };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
strategy.authenticate = function (req, options = {}) {
|
|
109
|
+
const self = this;
|
|
110
|
+
const samlResponse = req.body?.SAMLResponse || req.SAMLResponse;
|
|
111
|
+
|
|
112
|
+
if (samlResponse) {
|
|
113
|
+
handleAuthCallback({ SAMLResponse: samlResponse, RelayState: req.body?.RelayState || req.RelayState })
|
|
114
|
+
.then((result) => {
|
|
115
|
+
if (result.type === SAMLMessageType.AUTH_FAIL) {
|
|
116
|
+
return self.fail({ message: result.message, code: result.error });
|
|
117
|
+
}
|
|
118
|
+
if (verify) {
|
|
119
|
+
/** @param {Error|null} err - Error @param {Object|false} user - User @param {Object} info - Info @returns {void} */
|
|
120
|
+
const verified = (err, user, info) => {
|
|
121
|
+
if (err) return self.error(err);
|
|
122
|
+
if (!user) return self.fail(info || { message: "Verification failed" });
|
|
123
|
+
return self.success(user, info);
|
|
124
|
+
};
|
|
125
|
+
try {
|
|
126
|
+
if (passReqToCallback) verify(req, result.profile, verified);
|
|
127
|
+
else verify(result.profile, verified);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
return self.error(err);
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
return self.success(result.profile, { userId: result.userId });
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
.catch((err) => { if (typeof self.error === "function") self.error(err); });
|
|
136
|
+
} else {
|
|
137
|
+
handleAuthStart({ relayState: options.relayState || req.relayState || "" })
|
|
138
|
+
.then((result) => { self.redirect(result.url); })
|
|
139
|
+
.catch((err) => { if (typeof self.error === "function") self.error(err); });
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Register a test user for mock mode
|
|
145
|
+
* @param {string} nameId - User name ID
|
|
146
|
+
* @param {Object} [attributes={}] - User attributes
|
|
147
|
+
* @returns {Promise<boolean>} Success
|
|
148
|
+
*/
|
|
149
|
+
async function registerTestUser(nameId, attributes = {}) {
|
|
150
|
+
return storage.registerMockUser(nameId, attributes);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Cleanup resources
|
|
155
|
+
* @returns {void}
|
|
156
|
+
*/
|
|
157
|
+
function cleanup() {}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
name: strategy.name,
|
|
161
|
+
authenticate: strategy.authenticate,
|
|
162
|
+
handleAuthStart,
|
|
163
|
+
handleAuthCallback,
|
|
164
|
+
handleLogoutStart,
|
|
165
|
+
registerTestUser,
|
|
166
|
+
cleanup,
|
|
167
|
+
_config: { entryPoint, issuer, callbackUrl },
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const SAMLStrategy = createSAMLStrategy;
|
|
172
|
+
|
|
173
|
+
module.exports = { createSAMLStrategy, SAMLStrategy, SAMLMessageType, SAMLError };
|