agentshield-sdk 7.0.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/CHANGELOG.md +191 -0
- package/LICENSE +21 -0
- package/README.md +975 -0
- package/bin/agent-shield.js +680 -0
- package/package.json +118 -0
- package/src/adaptive.js +330 -0
- package/src/agent-protocol.js +998 -0
- package/src/alert-tuning.js +480 -0
- package/src/allowlist.js +603 -0
- package/src/audit-immutable.js +914 -0
- package/src/audit-streaming.js +469 -0
- package/src/badges.js +196 -0
- package/src/behavior-profiling.js +289 -0
- package/src/benchmark-harness.js +804 -0
- package/src/canary.js +271 -0
- package/src/certification.js +563 -0
- package/src/circuit-breaker.js +321 -0
- package/src/compliance.js +617 -0
- package/src/confidence-tuning.js +324 -0
- package/src/confused-deputy.js +624 -0
- package/src/context-scoring.js +360 -0
- package/src/conversation.js +494 -0
- package/src/cost-optimizer.js +1024 -0
- package/src/ctf.js +462 -0
- package/src/detector-core.js +1999 -0
- package/src/distributed.js +359 -0
- package/src/document-scanner.js +795 -0
- package/src/embedding.js +307 -0
- package/src/encoding.js +429 -0
- package/src/enterprise.js +405 -0
- package/src/errors.js +100 -0
- package/src/eu-ai-act.js +523 -0
- package/src/fuzzer.js +764 -0
- package/src/honeypot.js +328 -0
- package/src/i18n-patterns.js +523 -0
- package/src/index.js +430 -0
- package/src/integrations.js +528 -0
- package/src/llm-redteam.js +670 -0
- package/src/main.js +741 -0
- package/src/main.mjs +38 -0
- package/src/mcp-bridge.js +542 -0
- package/src/mcp-certification.js +846 -0
- package/src/mcp-sdk-integration.js +355 -0
- package/src/mcp-security-runtime.js +741 -0
- package/src/mcp-server.js +740 -0
- package/src/middleware.js +208 -0
- package/src/model-finetuning.js +884 -0
- package/src/model-fingerprint.js +1042 -0
- package/src/multi-agent-trust.js +453 -0
- package/src/multi-agent.js +404 -0
- package/src/multimodal.js +296 -0
- package/src/nist-mapping.js +505 -0
- package/src/observability.js +330 -0
- package/src/openclaw.js +450 -0
- package/src/otel.js +544 -0
- package/src/owasp-2025.js +483 -0
- package/src/pii.js +390 -0
- package/src/plugin-marketplace.js +628 -0
- package/src/plugin-system.js +349 -0
- package/src/policy-dsl.js +775 -0
- package/src/policy-extended.js +635 -0
- package/src/policy.js +443 -0
- package/src/presets.js +409 -0
- package/src/production.js +557 -0
- package/src/prompt-leakage.js +321 -0
- package/src/rag-vulnerability.js +579 -0
- package/src/redteam.js +475 -0
- package/src/response-handler.js +429 -0
- package/src/scanners.js +357 -0
- package/src/self-healing.js +363 -0
- package/src/semantic.js +339 -0
- package/src/shield-score.js +250 -0
- package/src/sso-saml.js +897 -0
- package/src/stream-scanner.js +806 -0
- package/src/testing.js +505 -0
- package/src/threat-encyclopedia.js +629 -0
- package/src/threat-intel-network.js +1017 -0
- package/src/token-analysis.js +467 -0
- package/src/tool-guard.js +412 -0
- package/src/tool-output-validator.js +354 -0
- package/src/utils.js +83 -0
- package/src/watermark.js +235 -0
- package/src/worker-scanner.js +601 -0
- package/types/index.d.ts +2088 -0
package/src/sso-saml.js
ADDED
|
@@ -0,0 +1,897 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agent Shield — SSO/SAML Integration
|
|
5
|
+
*
|
|
6
|
+
* Ties RBAC to enterprise identity providers. Supports SAML, OIDC, and LDAP
|
|
7
|
+
* provider types. All processing is local — no external calls are made.
|
|
8
|
+
*
|
|
9
|
+
* - SSOManager: Main SSO orchestration
|
|
10
|
+
* - SAMLParser: SAML assertion parsing and validation (simulated)
|
|
11
|
+
* - OIDCHandler: OpenID Connect flow handling
|
|
12
|
+
* - IdentityMapper: Maps IdP identities to Agent Shield RBAC roles
|
|
13
|
+
* - SSOSession: Session management with TTL and permissions
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const crypto = require('crypto');
|
|
17
|
+
|
|
18
|
+
// =========================================================================
|
|
19
|
+
// SSOSession
|
|
20
|
+
// =========================================================================
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Represents an authenticated SSO session.
|
|
24
|
+
*/
|
|
25
|
+
class SSOSession {
|
|
26
|
+
/**
|
|
27
|
+
* @param {object} identity - The user identity from the IdP.
|
|
28
|
+
* @param {string} role - The mapped Agent Shield RBAC role.
|
|
29
|
+
* @param {string[]} permissions - List of granted permissions.
|
|
30
|
+
* @param {number} ttl - Session time-to-live in milliseconds.
|
|
31
|
+
*/
|
|
32
|
+
constructor(identity, role, permissions, ttl) {
|
|
33
|
+
this.id = crypto.randomUUID ? crypto.randomUUID() : crypto.randomBytes(16).toString('hex');
|
|
34
|
+
this.identity = identity;
|
|
35
|
+
this.role = role;
|
|
36
|
+
this.permissions = permissions || [];
|
|
37
|
+
this.ttl = ttl || 3600000;
|
|
38
|
+
this.createdAt = Date.now();
|
|
39
|
+
this.expiresAt = this.createdAt + this.ttl;
|
|
40
|
+
this.revoked = false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Check whether the session is still valid (not expired, not revoked).
|
|
45
|
+
* @returns {boolean}
|
|
46
|
+
*/
|
|
47
|
+
isValid() {
|
|
48
|
+
if (this.revoked) return false;
|
|
49
|
+
return Date.now() < this.expiresAt;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Check whether the session has a specific permission.
|
|
54
|
+
* @param {string} permission - Permission string to check.
|
|
55
|
+
* @returns {boolean}
|
|
56
|
+
*/
|
|
57
|
+
hasPermission(permission) {
|
|
58
|
+
if (!this.isValid()) return false;
|
|
59
|
+
return this.permissions.includes('*') || this.permissions.includes(permission);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Serializable representation of the session.
|
|
64
|
+
* @returns {object}
|
|
65
|
+
*/
|
|
66
|
+
toJSON() {
|
|
67
|
+
return {
|
|
68
|
+
id: this.id,
|
|
69
|
+
identity: this.identity,
|
|
70
|
+
role: this.role,
|
|
71
|
+
permissions: this.permissions,
|
|
72
|
+
createdAt: new Date(this.createdAt).toISOString(),
|
|
73
|
+
expiresAt: new Date(this.expiresAt).toISOString(),
|
|
74
|
+
revoked: this.revoked,
|
|
75
|
+
valid: this.isValid()
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// =========================================================================
|
|
81
|
+
// IdentityMapper
|
|
82
|
+
// =========================================================================
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Default group-to-role mappings for common IdP groups.
|
|
86
|
+
*/
|
|
87
|
+
const DEFAULT_MAPPINGS = [
|
|
88
|
+
{ idpGroup: 'admins', shieldRole: 'admin', permissions: ['*'] },
|
|
89
|
+
{ idpGroup: 'security', shieldRole: 'analyst', permissions: ['scan', 'read', 'audit', 'configure', 'view_audit', 'manage_policies', 'view_reports'] },
|
|
90
|
+
{ idpGroup: 'developers', shieldRole: 'operator', permissions: ['scan', 'read', 'configure', 'view_reports'] },
|
|
91
|
+
{ idpGroup: '*', shieldRole: 'viewer', permissions: ['read', 'view_reports'] }
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Maps IdP identities (groups/attributes) to Agent Shield RBAC roles.
|
|
96
|
+
*/
|
|
97
|
+
class IdentityMapper {
|
|
98
|
+
/**
|
|
99
|
+
* @param {Array<{idpGroup: string, shieldRole: string, permissions: string[]}>} [mappingRules]
|
|
100
|
+
*/
|
|
101
|
+
constructor(mappingRules) {
|
|
102
|
+
this.rules = mappingRules ? [...mappingRules] : [...DEFAULT_MAPPINGS];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Add a mapping rule.
|
|
107
|
+
* @param {{idpGroup: string, shieldRole: string, permissions: string[]}} rule
|
|
108
|
+
*/
|
|
109
|
+
addRule(rule) {
|
|
110
|
+
if (!rule || !rule.idpGroup || !rule.shieldRole) {
|
|
111
|
+
throw new Error('[Agent Shield] Mapping rule must have idpGroup and shieldRole');
|
|
112
|
+
}
|
|
113
|
+
// Insert before the wildcard rule if one exists
|
|
114
|
+
const wildcardIdx = this.rules.findIndex(r => r.idpGroup === '*');
|
|
115
|
+
if (wildcardIdx >= 0) {
|
|
116
|
+
this.rules.splice(wildcardIdx, 0, rule);
|
|
117
|
+
} else {
|
|
118
|
+
this.rules.push(rule);
|
|
119
|
+
}
|
|
120
|
+
return this;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Map an identity to a role and permissions based on the identity's groups.
|
|
125
|
+
* @param {object} identity - Identity object with a groups array.
|
|
126
|
+
* @returns {{role: string, permissions: string[]}}
|
|
127
|
+
*/
|
|
128
|
+
mapIdentity(identity) {
|
|
129
|
+
const groups = identity.groups || [];
|
|
130
|
+
let matchedRole = null;
|
|
131
|
+
let matchedPermissions = [];
|
|
132
|
+
|
|
133
|
+
for (const rule of this.rules) {
|
|
134
|
+
if (rule.idpGroup === '*' || groups.includes(rule.idpGroup)) {
|
|
135
|
+
matchedRole = rule.shieldRole;
|
|
136
|
+
matchedPermissions = [...(rule.permissions || [])];
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Fallback to viewer if no rule matched
|
|
142
|
+
if (!matchedRole) {
|
|
143
|
+
matchedRole = 'viewer';
|
|
144
|
+
matchedPermissions = ['view_reports'];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return { role: matchedRole, permissions: matchedPermissions };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// =========================================================================
|
|
152
|
+
// SAMLParser
|
|
153
|
+
// =========================================================================
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Simulated SAML assertion parser. Extracts identity attributes from
|
|
157
|
+
* SAML-like XML strings without requiring real XML crypto libraries.
|
|
158
|
+
*/
|
|
159
|
+
class SAMLParser {
|
|
160
|
+
/**
|
|
161
|
+
* Parse a SAML assertion XML string and extract identity attributes.
|
|
162
|
+
* @param {string} xml - SAML assertion XML string.
|
|
163
|
+
* @returns {object} Parsed assertion with issuer, subject, attributes, conditions.
|
|
164
|
+
*/
|
|
165
|
+
parseAssertion(xml) {
|
|
166
|
+
if (!xml || typeof xml !== 'string') {
|
|
167
|
+
throw new Error('[Agent Shield] SAML assertion must be a non-empty string');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const assertion = {
|
|
171
|
+
issuer: this._extractTag(xml, 'Issuer'),
|
|
172
|
+
subject: {
|
|
173
|
+
nameId: this._extractTag(xml, 'NameID'),
|
|
174
|
+
format: this._extractAttribute(xml, 'NameID', 'Format')
|
|
175
|
+
},
|
|
176
|
+
conditions: {
|
|
177
|
+
notBefore: this._extractAttribute(xml, 'Conditions', 'NotBefore'),
|
|
178
|
+
notOnOrAfter: this._extractAttribute(xml, 'Conditions', 'NotOnOrAfter'),
|
|
179
|
+
audience: this._extractTag(xml, 'Audience')
|
|
180
|
+
},
|
|
181
|
+
attributes: this.extractAttributes({ xml }),
|
|
182
|
+
raw: xml
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
return assertion;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Validate a parsed SAML assertion against a provider configuration.
|
|
190
|
+
* Checks issuer, audience, time validity, subject, and XML signature.
|
|
191
|
+
* @param {object} assertion - Parsed assertion from parseAssertion.
|
|
192
|
+
* @param {object} provider - Provider config with {issuer, audience, certificate}.
|
|
193
|
+
* @returns {{valid: boolean, errors: string[], signatureVerified: boolean}}
|
|
194
|
+
*/
|
|
195
|
+
validateAssertion(assertion, provider) {
|
|
196
|
+
const errors = [];
|
|
197
|
+
let signatureVerified = false;
|
|
198
|
+
|
|
199
|
+
// Check issuer
|
|
200
|
+
if (assertion.issuer && provider.metadata && provider.metadata.issuer) {
|
|
201
|
+
if (assertion.issuer !== provider.metadata.issuer) {
|
|
202
|
+
errors.push(`Issuer mismatch: expected "${provider.metadata.issuer}", got "${assertion.issuer}"`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Check audience
|
|
207
|
+
if (assertion.conditions && assertion.conditions.audience && provider.metadata && provider.metadata.audience) {
|
|
208
|
+
if (assertion.conditions.audience !== provider.metadata.audience) {
|
|
209
|
+
errors.push(`Audience mismatch: expected "${provider.metadata.audience}", got "${assertion.conditions.audience}"`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Check time validity
|
|
214
|
+
const now = new Date();
|
|
215
|
+
if (assertion.conditions && assertion.conditions.notBefore) {
|
|
216
|
+
const notBefore = new Date(assertion.conditions.notBefore);
|
|
217
|
+
if (now < notBefore) {
|
|
218
|
+
errors.push(`Assertion not yet valid (notBefore: ${assertion.conditions.notBefore})`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (assertion.conditions && assertion.conditions.notOnOrAfter) {
|
|
222
|
+
const notOnOrAfter = new Date(assertion.conditions.notOnOrAfter);
|
|
223
|
+
if (now >= notOnOrAfter) {
|
|
224
|
+
errors.push(`Assertion expired (notOnOrAfter: ${assertion.conditions.notOnOrAfter})`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Check subject
|
|
229
|
+
if (!assertion.subject || !assertion.subject.nameId) {
|
|
230
|
+
errors.push('Missing subject NameID');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Verify XML signature if certificate is provided
|
|
234
|
+
if (provider.metadata && provider.metadata.certificate) {
|
|
235
|
+
const sigResult = this.verifySignature(assertion.raw, provider.metadata.certificate);
|
|
236
|
+
signatureVerified = sigResult.valid;
|
|
237
|
+
if (!sigResult.valid) {
|
|
238
|
+
errors.push(`Signature verification failed: ${sigResult.error}`);
|
|
239
|
+
}
|
|
240
|
+
} else {
|
|
241
|
+
errors.push('No IdP certificate provided — cannot verify assertion signature. This is a security risk.');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return { valid: errors.length === 0, errors, signatureVerified };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Verify the XML digital signature on a SAML assertion.
|
|
249
|
+
* Uses Node.js crypto to validate RSA-SHA256 or RSA-SHA1 signatures
|
|
250
|
+
* against the IdP's public certificate.
|
|
251
|
+
*
|
|
252
|
+
* @param {string} xml - Raw SAML assertion XML.
|
|
253
|
+
* @param {string} certificate - PEM-encoded X.509 certificate from IdP metadata.
|
|
254
|
+
* @returns {{valid: boolean, error: string|null, algorithm: string|null}}
|
|
255
|
+
*/
|
|
256
|
+
verifySignature(xml, certificate) {
|
|
257
|
+
if (!xml || !certificate) {
|
|
258
|
+
return { valid: false, error: 'Missing XML or certificate', algorithm: null };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Extract the SignatureValue from the XML
|
|
262
|
+
const sigValueMatch = xml.match(/<(?:ds:)?SignatureValue[^>]*>([\s\S]*?)<\/(?:ds:)?SignatureValue>/i);
|
|
263
|
+
if (!sigValueMatch) {
|
|
264
|
+
return { valid: false, error: 'No SignatureValue element found in assertion', algorithm: null };
|
|
265
|
+
}
|
|
266
|
+
const signatureB64 = sigValueMatch[1].replace(/\s+/g, '');
|
|
267
|
+
|
|
268
|
+
// Extract the SignedInfo block (the data that was signed)
|
|
269
|
+
const signedInfoMatch = xml.match(/<(?:ds:)?SignedInfo[^>]*>([\s\S]*?)<\/(?:ds:)?SignedInfo>/i);
|
|
270
|
+
if (!signedInfoMatch) {
|
|
271
|
+
return { valid: false, error: 'No SignedInfo element found in assertion', algorithm: null };
|
|
272
|
+
}
|
|
273
|
+
// Reconstruct the canonicalized SignedInfo element
|
|
274
|
+
const signedInfoXml = xml.match(/<(?:ds:)?SignedInfo[^>]*>[\s\S]*?<\/(?:ds:)?SignedInfo>/i)[0];
|
|
275
|
+
|
|
276
|
+
// Detect signing algorithm
|
|
277
|
+
const algMatch = xml.match(/Algorithm="([^"]*(?:rsa-sha(?:1|256|384|512))[^"]*)"/i);
|
|
278
|
+
let algorithm = 'RSA-SHA256'; // default
|
|
279
|
+
if (algMatch) {
|
|
280
|
+
const algUri = algMatch[1].toLowerCase();
|
|
281
|
+
if (algUri.includes('sha512')) algorithm = 'RSA-SHA512';
|
|
282
|
+
else if (algUri.includes('sha384')) algorithm = 'RSA-SHA384';
|
|
283
|
+
else if (algUri.includes('sha256')) algorithm = 'RSA-SHA256';
|
|
284
|
+
else if (algUri.includes('sha1')) algorithm = 'RSA-SHA1';
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Map algorithm name to Node.js crypto algorithm
|
|
288
|
+
const cryptoAlg = algorithm.replace('RSA-', '').toLowerCase().replace('-', '');
|
|
289
|
+
|
|
290
|
+
// Normalize certificate to PEM format
|
|
291
|
+
let pem = certificate.trim();
|
|
292
|
+
if (!pem.startsWith('-----BEGIN')) {
|
|
293
|
+
pem = '-----BEGIN CERTIFICATE-----\n' + pem.replace(/(.{64})/g, '$1\n').trim() + '\n-----END CERTIFICATE-----';
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
const verifier = crypto.createVerify(cryptoAlg);
|
|
298
|
+
verifier.update(signedInfoXml);
|
|
299
|
+
const signatureBuffer = Buffer.from(signatureB64, 'base64');
|
|
300
|
+
const isValid = verifier.verify(pem, signatureBuffer);
|
|
301
|
+
return { valid: isValid, error: isValid ? null : 'Signature does not match', algorithm };
|
|
302
|
+
} catch (e) {
|
|
303
|
+
return { valid: false, error: `Crypto error: ${e.message}`, algorithm };
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Extract user attributes (email, name, groups, roles) from a SAML assertion.
|
|
309
|
+
* @param {object} assertion - Object with either raw xml or pre-parsed attributes.
|
|
310
|
+
* @returns {object} Extracted attributes {email, name, groups, roles, custom}.
|
|
311
|
+
*/
|
|
312
|
+
extractAttributes(assertion) {
|
|
313
|
+
const xml = assertion.xml || assertion.raw || '';
|
|
314
|
+
const attributes = {
|
|
315
|
+
email: null,
|
|
316
|
+
name: null,
|
|
317
|
+
groups: [],
|
|
318
|
+
roles: [],
|
|
319
|
+
custom: {}
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
// Extract email
|
|
323
|
+
const emailPatterns = [
|
|
324
|
+
/AttributeName="email"[^>]*>\s*<[^>]*AttributeValue[^>]*>([^<]+)/i,
|
|
325
|
+
/AttributeName="http:\/\/schemas\.xmlsoap\.org\/ws\/2005\/05\/identity\/claims\/emailaddress"[^>]*>\s*<[^>]*AttributeValue[^>]*>([^<]+)/i,
|
|
326
|
+
/AttributeName="mail"[^>]*>\s*<[^>]*AttributeValue[^>]*>([^<]+)/i
|
|
327
|
+
];
|
|
328
|
+
for (const pattern of emailPatterns) {
|
|
329
|
+
const match = xml.match(pattern);
|
|
330
|
+
if (match) { attributes.email = match[1].trim(); break; }
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Extract name
|
|
334
|
+
const namePatterns = [
|
|
335
|
+
/AttributeName="displayName"[^>]*>\s*<[^>]*AttributeValue[^>]*>([^<]+)/i,
|
|
336
|
+
/AttributeName="name"[^>]*>\s*<[^>]*AttributeValue[^>]*>([^<]+)/i,
|
|
337
|
+
/AttributeName="http:\/\/schemas\.xmlsoap\.org\/ws\/2005\/05\/identity\/claims\/name"[^>]*>\s*<[^>]*AttributeValue[^>]*>([^<]+)/i
|
|
338
|
+
];
|
|
339
|
+
for (const pattern of namePatterns) {
|
|
340
|
+
const match = xml.match(pattern);
|
|
341
|
+
if (match) { attributes.name = match[1].trim(); break; }
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Extract groups
|
|
345
|
+
const groupPattern = /AttributeName="(?:groups?|memberOf|http:\/\/schemas\.xmlsoap\.org\/claims\/Group)"[^>]*>([\s\S]*?)<\/(?:saml2?:)?Attribute>/gi;
|
|
346
|
+
const groupMatch = xml.match(groupPattern);
|
|
347
|
+
if (groupMatch) {
|
|
348
|
+
for (const block of groupMatch) {
|
|
349
|
+
const valuePattern = /<[^>]*AttributeValue[^>]*>([^<]+)/gi;
|
|
350
|
+
let valMatch;
|
|
351
|
+
while ((valMatch = valuePattern.exec(block)) !== null) {
|
|
352
|
+
attributes.groups.push(valMatch[1].trim());
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Extract roles
|
|
358
|
+
const rolePattern = /AttributeName="(?:roles?|http:\/\/schemas\.microsoft\.com\/ws\/2008\/06\/identity\/claims\/role)"[^>]*>([\s\S]*?)<\/(?:saml2?:)?Attribute>/gi;
|
|
359
|
+
const roleMatch = xml.match(rolePattern);
|
|
360
|
+
if (roleMatch) {
|
|
361
|
+
for (const block of roleMatch) {
|
|
362
|
+
const valuePattern = /<[^>]*AttributeValue[^>]*>([^<]+)/gi;
|
|
363
|
+
let valMatch;
|
|
364
|
+
while ((valMatch = valuePattern.exec(block)) !== null) {
|
|
365
|
+
attributes.roles.push(valMatch[1].trim());
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return attributes;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Build a SAML AuthnRequest for the given provider.
|
|
375
|
+
* @param {object} provider - Provider config with metadata {ssoUrl, entityId, acsUrl}.
|
|
376
|
+
* @returns {string} SAML AuthnRequest XML string.
|
|
377
|
+
*/
|
|
378
|
+
buildAuthnRequest(provider) {
|
|
379
|
+
const id = '_' + crypto.randomBytes(16).toString('hex');
|
|
380
|
+
const issueInstant = new Date().toISOString();
|
|
381
|
+
const metadata = provider.metadata || {};
|
|
382
|
+
|
|
383
|
+
return [
|
|
384
|
+
'<samlp:AuthnRequest',
|
|
385
|
+
' xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"',
|
|
386
|
+
' xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"',
|
|
387
|
+
` ID="${id}"`,
|
|
388
|
+
' Version="2.0"',
|
|
389
|
+
` IssueInstant="${issueInstant}"`,
|
|
390
|
+
` Destination="${metadata.ssoUrl || ''}"`,
|
|
391
|
+
` AssertionConsumerServiceURL="${metadata.acsUrl || ''}"`,
|
|
392
|
+
' ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST">',
|
|
393
|
+
` <saml:Issuer>${metadata.entityId || ''}</saml:Issuer>`,
|
|
394
|
+
' <samlp:NameIDPolicy',
|
|
395
|
+
' Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"',
|
|
396
|
+
' AllowCreate="true"/>',
|
|
397
|
+
'</samlp:AuthnRequest>'
|
|
398
|
+
].join('\n');
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// --- Internal helpers ---
|
|
402
|
+
|
|
403
|
+
_extractTag(xml, tagName) {
|
|
404
|
+
const pattern = new RegExp(`<(?:[\\w-]+:)?${tagName}[^>]*>([^<]+)<\\/`, 'i');
|
|
405
|
+
const match = xml.match(pattern);
|
|
406
|
+
return match ? match[1].trim() : null;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
_extractAttribute(xml, tagName, attrName) {
|
|
410
|
+
const pattern = new RegExp(`<(?:[\\w-]+:)?${tagName}[^>]*${attrName}="([^"]*)"`, 'i');
|
|
411
|
+
const match = xml.match(pattern);
|
|
412
|
+
return match ? match[1].trim() : null;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// =========================================================================
|
|
417
|
+
// OIDCHandler
|
|
418
|
+
// =========================================================================
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Simulated OpenID Connect handler. Builds auth URLs, exchanges codes,
|
|
422
|
+
* and validates JWT-like id_tokens — all locally, no external calls.
|
|
423
|
+
*/
|
|
424
|
+
class OIDCHandler {
|
|
425
|
+
/**
|
|
426
|
+
* @param {object} config
|
|
427
|
+
* @param {string} config.clientId - OIDC client ID.
|
|
428
|
+
* @param {string} config.issuer - OIDC issuer URL.
|
|
429
|
+
* @param {string} config.redirectUri - Redirect URI after authentication.
|
|
430
|
+
*/
|
|
431
|
+
constructor(config = {}) {
|
|
432
|
+
this.clientId = config.clientId || '';
|
|
433
|
+
this.issuer = config.issuer || '';
|
|
434
|
+
this.redirectUri = config.redirectUri || '';
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Build an OIDC authorization URL.
|
|
439
|
+
* @param {string} state - Opaque state parameter for CSRF protection.
|
|
440
|
+
* @returns {string} Authorization URL.
|
|
441
|
+
*/
|
|
442
|
+
buildAuthorizationUrl(state) {
|
|
443
|
+
const nonce = crypto.randomBytes(16).toString('hex');
|
|
444
|
+
const params = new URLSearchParams({
|
|
445
|
+
response_type: 'code',
|
|
446
|
+
client_id: this.clientId,
|
|
447
|
+
redirect_uri: this.redirectUri,
|
|
448
|
+
scope: 'openid profile email groups',
|
|
449
|
+
state: state || crypto.randomBytes(16).toString('hex'),
|
|
450
|
+
nonce
|
|
451
|
+
});
|
|
452
|
+
return `${this.issuer}/authorize?${params.toString()}`;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Simulate an authorization code exchange. Returns token-like objects.
|
|
457
|
+
* @param {string} code - The authorization code.
|
|
458
|
+
* @returns {object} Token response {access_token, id_token, token_type, expires_in}.
|
|
459
|
+
*/
|
|
460
|
+
exchangeCode(code) {
|
|
461
|
+
if (!code || typeof code !== 'string') {
|
|
462
|
+
throw new Error('[Agent Shield] Authorization code is required');
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const now = Math.floor(Date.now() / 1000);
|
|
466
|
+
const payload = {
|
|
467
|
+
iss: this.issuer,
|
|
468
|
+
sub: 'user_' + code.substring(0, 8),
|
|
469
|
+
aud: this.clientId,
|
|
470
|
+
iat: now,
|
|
471
|
+
exp: now + 3600,
|
|
472
|
+
nonce: crypto.randomBytes(8).toString('hex')
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
const header = Buffer.from(JSON.stringify({ alg: 'RS256', typ: 'JWT' })).toString('base64url');
|
|
476
|
+
const body = Buffer.from(JSON.stringify(payload)).toString('base64url');
|
|
477
|
+
const signature = crypto.randomBytes(32).toString('base64url');
|
|
478
|
+
|
|
479
|
+
const idToken = `${header}.${body}.${signature}`;
|
|
480
|
+
const accessToken = crypto.randomBytes(32).toString('base64url');
|
|
481
|
+
|
|
482
|
+
return {
|
|
483
|
+
access_token: accessToken,
|
|
484
|
+
id_token: idToken,
|
|
485
|
+
token_type: 'Bearer',
|
|
486
|
+
expires_in: 3600
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Validate a JWT id_token. Decodes, checks claims, and verifies signature
|
|
492
|
+
* if a signing key or JWKS is configured.
|
|
493
|
+
*
|
|
494
|
+
* @param {string} token - JWT id_token string.
|
|
495
|
+
* @param {object} [options] - Validation options.
|
|
496
|
+
* @param {string} [options.signingKey] - PEM public key or certificate for RS256 verification.
|
|
497
|
+
* @param {string} [options.secret] - Shared secret for HS256 verification.
|
|
498
|
+
* @param {string} [options.nonce] - Expected nonce value for replay protection.
|
|
499
|
+
* @returns {{valid: boolean, claims: object, errors: string[], signatureVerified: boolean}}
|
|
500
|
+
*/
|
|
501
|
+
validateIdToken(token, options = {}) {
|
|
502
|
+
const errors = [];
|
|
503
|
+
let signatureVerified = false;
|
|
504
|
+
|
|
505
|
+
if (!token || typeof token !== 'string') {
|
|
506
|
+
return { valid: false, claims: null, errors: ['Token must be a non-empty string'], signatureVerified: false };
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const parts = token.split('.');
|
|
510
|
+
if (parts.length !== 3) {
|
|
511
|
+
return { valid: false, claims: null, errors: ['Invalid JWT structure: expected 3 parts'], signatureVerified: false };
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Decode header
|
|
515
|
+
let header;
|
|
516
|
+
try {
|
|
517
|
+
header = JSON.parse(Buffer.from(parts[0], 'base64url').toString('utf-8'));
|
|
518
|
+
} catch (e) {
|
|
519
|
+
return { valid: false, claims: null, errors: ['Failed to decode JWT header'], signatureVerified: false };
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Decode claims
|
|
523
|
+
let claims;
|
|
524
|
+
try {
|
|
525
|
+
claims = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf-8'));
|
|
526
|
+
} catch (e) {
|
|
527
|
+
return { valid: false, claims: null, errors: ['Failed to decode JWT payload'], signatureVerified: false };
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Verify signature
|
|
531
|
+
const signingInput = parts[0] + '.' + parts[1];
|
|
532
|
+
const signature = parts[2];
|
|
533
|
+
const alg = (header.alg || '').toUpperCase();
|
|
534
|
+
|
|
535
|
+
if (options.signingKey && (alg === 'RS256' || alg === 'RS384' || alg === 'RS512')) {
|
|
536
|
+
// RSA signature verification
|
|
537
|
+
const hashAlg = alg.replace('RS', 'sha');
|
|
538
|
+
try {
|
|
539
|
+
const verifier = crypto.createVerify(hashAlg);
|
|
540
|
+
verifier.update(signingInput);
|
|
541
|
+
const sigBuf = Buffer.from(signature, 'base64url');
|
|
542
|
+
signatureVerified = verifier.verify(options.signingKey, sigBuf);
|
|
543
|
+
if (!signatureVerified) {
|
|
544
|
+
errors.push('JWT signature verification failed — token may be forged');
|
|
545
|
+
}
|
|
546
|
+
} catch (e) {
|
|
547
|
+
errors.push(`JWT signature verification error: ${e.message}`);
|
|
548
|
+
}
|
|
549
|
+
} else if (options.secret && (alg === 'HS256' || alg === 'HS384' || alg === 'HS512')) {
|
|
550
|
+
// HMAC signature verification
|
|
551
|
+
const hashAlg = alg.replace('HS', 'sha');
|
|
552
|
+
const expectedSig = crypto.createHmac(hashAlg, options.secret).update(signingInput).digest('base64url');
|
|
553
|
+
signatureVerified = crypto.timingSafeEqual(
|
|
554
|
+
Buffer.from(signature, 'utf-8'),
|
|
555
|
+
Buffer.from(expectedSig, 'utf-8')
|
|
556
|
+
);
|
|
557
|
+
if (!signatureVerified) {
|
|
558
|
+
errors.push('JWT HMAC signature verification failed — token may be forged');
|
|
559
|
+
}
|
|
560
|
+
} else if (alg === 'NONE' || alg === '') {
|
|
561
|
+
errors.push('JWT uses "none" algorithm — this is insecure and rejected');
|
|
562
|
+
} else if (!options.signingKey && !options.secret) {
|
|
563
|
+
errors.push('No signing key provided — JWT signature not verified. This is a security risk.');
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Validate issuer
|
|
567
|
+
if (this.issuer && claims.iss !== this.issuer) {
|
|
568
|
+
errors.push(`Issuer mismatch: expected "${this.issuer}", got "${claims.iss}"`);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Validate audience
|
|
572
|
+
if (this.clientId) {
|
|
573
|
+
const aud = Array.isArray(claims.aud) ? claims.aud : [claims.aud];
|
|
574
|
+
if (!aud.includes(this.clientId)) {
|
|
575
|
+
errors.push(`Audience mismatch: expected "${this.clientId}", got "${claims.aud}"`);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Validate expiration
|
|
580
|
+
const now = Math.floor(Date.now() / 1000);
|
|
581
|
+
if (claims.exp && claims.exp < now) {
|
|
582
|
+
errors.push('Token has expired');
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Validate issued-at is not in the future (with 60s clock skew tolerance)
|
|
586
|
+
if (claims.iat && claims.iat > now + 60) {
|
|
587
|
+
errors.push('Token issued-at is in the future');
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Validate nonce (replay protection)
|
|
591
|
+
if (options.nonce && claims.nonce !== options.nonce) {
|
|
592
|
+
errors.push(`Nonce mismatch: possible replay attack`);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return { valid: errors.length === 0, claims, errors, signatureVerified };
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Extract user info from an access token or id_token.
|
|
600
|
+
* @param {string} accessToken - Access token or id_token.
|
|
601
|
+
* @returns {object} User info {sub, email, name, groups}.
|
|
602
|
+
*/
|
|
603
|
+
getUserInfo(accessToken) {
|
|
604
|
+
if (!accessToken || typeof accessToken !== 'string') {
|
|
605
|
+
throw new Error('[Agent Shield] Access token is required');
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Try to decode as JWT
|
|
609
|
+
const parts = accessToken.split('.');
|
|
610
|
+
if (parts.length === 3) {
|
|
611
|
+
try {
|
|
612
|
+
const claims = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf-8'));
|
|
613
|
+
return {
|
|
614
|
+
sub: claims.sub || null,
|
|
615
|
+
email: claims.email || null,
|
|
616
|
+
name: claims.name || null,
|
|
617
|
+
groups: claims.groups || []
|
|
618
|
+
};
|
|
619
|
+
} catch (e) {
|
|
620
|
+
// Not a valid JWT, return minimal info
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Fallback: return a stub based on the token
|
|
625
|
+
return {
|
|
626
|
+
sub: 'user_' + accessToken.substring(0, 8),
|
|
627
|
+
email: null,
|
|
628
|
+
name: null,
|
|
629
|
+
groups: []
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// =========================================================================
|
|
635
|
+
// SSOManager
|
|
636
|
+
// =========================================================================
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Main SSO orchestration. Registers identity providers, authenticates
|
|
640
|
+
* users, manages sessions, and maps identities to Agent Shield RBAC roles.
|
|
641
|
+
*/
|
|
642
|
+
class SSOManager {
|
|
643
|
+
/**
|
|
644
|
+
* @param {object} [config]
|
|
645
|
+
* @param {Array} [config.providers] - Pre-registered IdP configurations.
|
|
646
|
+
* @param {string} [config.defaultRole] - Default role when no mapping matches.
|
|
647
|
+
* @param {number} [config.sessionTTL] - Session TTL in milliseconds.
|
|
648
|
+
* @param {boolean} [config.auditLog] - Whether to log authentication events.
|
|
649
|
+
*/
|
|
650
|
+
constructor(config = {}) {
|
|
651
|
+
this.providers = new Map();
|
|
652
|
+
this.sessions = new Map();
|
|
653
|
+
this.auditEntries = [];
|
|
654
|
+
this.defaultRole = config.defaultRole || 'viewer';
|
|
655
|
+
this.sessionTTL = config.sessionTTL || 3600000;
|
|
656
|
+
this.auditLog = config.auditLog !== undefined ? config.auditLog : true;
|
|
657
|
+
this.identityMapper = new IdentityMapper();
|
|
658
|
+
this.samlParser = new SAMLParser();
|
|
659
|
+
this.oidcHandler = null;
|
|
660
|
+
|
|
661
|
+
// Register any providers passed in config
|
|
662
|
+
if (Array.isArray(config.providers)) {
|
|
663
|
+
for (const provider of config.providers) {
|
|
664
|
+
this.registerProvider(provider);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
console.log('[Agent Shield] SSOManager initialized');
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Register an identity provider configuration.
|
|
673
|
+
* @param {object} provider - Provider config {id, type, name, metadata}.
|
|
674
|
+
* @returns {SSOManager}
|
|
675
|
+
*/
|
|
676
|
+
registerProvider(provider) {
|
|
677
|
+
if (!provider || !provider.id || !provider.type) {
|
|
678
|
+
throw new Error('[Agent Shield] Provider must have id and type');
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const validTypes = ['saml', 'oidc', 'ldap'];
|
|
682
|
+
if (!validTypes.includes(provider.type)) {
|
|
683
|
+
throw new Error(`[Agent Shield] Invalid provider type: ${provider.type}. Must be one of: ${validTypes.join(', ')}`);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
this.providers.set(provider.id, {
|
|
687
|
+
...provider,
|
|
688
|
+
registeredAt: new Date().toISOString()
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
// Initialize OIDC handler if provider is OIDC
|
|
692
|
+
if (provider.type === 'oidc' && provider.metadata) {
|
|
693
|
+
this.oidcHandler = new OIDCHandler({
|
|
694
|
+
clientId: provider.metadata.clientId,
|
|
695
|
+
issuer: provider.metadata.issuer,
|
|
696
|
+
redirectUri: provider.metadata.redirectUri
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
this._audit('provider_registered', { providerId: provider.id, type: provider.type });
|
|
701
|
+
console.log(`[Agent Shield] SSO provider registered: ${provider.name || provider.id} (${provider.type})`);
|
|
702
|
+
|
|
703
|
+
return this;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Authenticate a user via SAML assertion or OIDC token.
|
|
708
|
+
* @param {string} providerType - 'saml' or 'oidc'.
|
|
709
|
+
* @param {string} assertion - SAML XML assertion or OIDC code/token.
|
|
710
|
+
* @returns {SSOSession}
|
|
711
|
+
*/
|
|
712
|
+
authenticate(providerType, assertion) {
|
|
713
|
+
if (!assertion) {
|
|
714
|
+
throw new Error('[Agent Shield] Assertion/token is required');
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
let identity;
|
|
718
|
+
|
|
719
|
+
if (providerType === 'saml') {
|
|
720
|
+
identity = this._authenticateSAML(assertion);
|
|
721
|
+
} else if (providerType === 'oidc') {
|
|
722
|
+
identity = this._authenticateOIDC(assertion);
|
|
723
|
+
} else {
|
|
724
|
+
throw new Error(`[Agent Shield] Unsupported provider type for authentication: ${providerType}`);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// Map identity to role
|
|
728
|
+
const mapping = this.mapToRole(identity);
|
|
729
|
+
const session = new SSOSession(identity, mapping.role, mapping.permissions, this.sessionTTL);
|
|
730
|
+
|
|
731
|
+
this.sessions.set(session.id, session);
|
|
732
|
+
this._audit('authentication_success', {
|
|
733
|
+
sessionId: session.id,
|
|
734
|
+
email: identity.email,
|
|
735
|
+
role: mapping.role,
|
|
736
|
+
providerType
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
console.log(`[Agent Shield] SSO authentication successful: ${identity.email || identity.nameId} -> ${mapping.role}`);
|
|
740
|
+
|
|
741
|
+
return session;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Map an identity to an Agent Shield RBAC role.
|
|
746
|
+
* @param {object} identity - Identity with groups array.
|
|
747
|
+
* @returns {{role: string, permissions: string[]}}
|
|
748
|
+
*/
|
|
749
|
+
mapToRole(identity) {
|
|
750
|
+
return this.identityMapper.mapIdentity(identity);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Retrieve an active session by ID.
|
|
755
|
+
* @param {string} sessionId
|
|
756
|
+
* @returns {SSOSession|null}
|
|
757
|
+
*/
|
|
758
|
+
getSession(sessionId) {
|
|
759
|
+
const session = this.sessions.get(sessionId);
|
|
760
|
+
if (!session) return null;
|
|
761
|
+
if (!session.isValid()) {
|
|
762
|
+
this.sessions.delete(sessionId);
|
|
763
|
+
return null;
|
|
764
|
+
}
|
|
765
|
+
return session;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Revoke an active session.
|
|
770
|
+
* @param {string} sessionId
|
|
771
|
+
* @returns {boolean} Whether the session was found and revoked.
|
|
772
|
+
*/
|
|
773
|
+
revokeSession(sessionId) {
|
|
774
|
+
const session = this.sessions.get(sessionId);
|
|
775
|
+
if (!session) return false;
|
|
776
|
+
|
|
777
|
+
session.revoked = true;
|
|
778
|
+
this._audit('session_revoked', { sessionId });
|
|
779
|
+
console.log(`[Agent Shield] Session revoked: ${sessionId}`);
|
|
780
|
+
|
|
781
|
+
return true;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* List all active (non-expired, non-revoked) sessions.
|
|
786
|
+
* @returns {SSOSession[]}
|
|
787
|
+
*/
|
|
788
|
+
listActiveSessions() {
|
|
789
|
+
const active = [];
|
|
790
|
+
for (const [id, session] of this.sessions) {
|
|
791
|
+
if (session.isValid()) {
|
|
792
|
+
active.push(session);
|
|
793
|
+
} else {
|
|
794
|
+
this.sessions.delete(id);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
return active;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Get the authentication audit log.
|
|
802
|
+
* @returns {Array<object>}
|
|
803
|
+
*/
|
|
804
|
+
getAuditLog() {
|
|
805
|
+
return [...this.auditEntries];
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// --- Internal helpers ---
|
|
809
|
+
|
|
810
|
+
_authenticateSAML(xml) {
|
|
811
|
+
const assertion = this.samlParser.parseAssertion(xml);
|
|
812
|
+
|
|
813
|
+
// Find a SAML provider to validate against
|
|
814
|
+
let samlProvider = null;
|
|
815
|
+
for (const [, provider] of this.providers) {
|
|
816
|
+
if (provider.type === 'saml') {
|
|
817
|
+
samlProvider = provider;
|
|
818
|
+
break;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
if (samlProvider) {
|
|
823
|
+
const validation = this.samlParser.validateAssertion(assertion, samlProvider);
|
|
824
|
+
if (!validation.valid) {
|
|
825
|
+
this._audit('authentication_failed', { errors: validation.errors, providerType: 'saml' });
|
|
826
|
+
throw new Error(`[Agent Shield] SAML validation failed: ${validation.errors.join('; ')}`);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
return {
|
|
831
|
+
nameId: assertion.subject.nameId,
|
|
832
|
+
email: assertion.attributes.email || assertion.subject.nameId,
|
|
833
|
+
name: assertion.attributes.name,
|
|
834
|
+
groups: assertion.attributes.groups || [],
|
|
835
|
+
roles: assertion.attributes.roles || [],
|
|
836
|
+
provider: 'saml'
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
_authenticateOIDC(codeOrToken) {
|
|
841
|
+
// If it looks like a JWT (has dots), validate directly
|
|
842
|
+
if (codeOrToken.includes('.')) {
|
|
843
|
+
const handler = this.oidcHandler || new OIDCHandler({});
|
|
844
|
+
const validation = handler.validateIdToken(codeOrToken);
|
|
845
|
+
if (!validation.valid) {
|
|
846
|
+
this._audit('authentication_failed', { errors: validation.errors, providerType: 'oidc' });
|
|
847
|
+
throw new Error(`[Agent Shield] OIDC token validation failed: ${validation.errors.join('; ')}`);
|
|
848
|
+
}
|
|
849
|
+
const claims = validation.claims;
|
|
850
|
+
return {
|
|
851
|
+
sub: claims.sub,
|
|
852
|
+
email: claims.email || null,
|
|
853
|
+
name: claims.name || null,
|
|
854
|
+
groups: claims.groups || [],
|
|
855
|
+
roles: claims.roles || [],
|
|
856
|
+
provider: 'oidc'
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Otherwise treat as authorization code
|
|
861
|
+
const handler = this.oidcHandler || new OIDCHandler({});
|
|
862
|
+
const tokens = handler.exchangeCode(codeOrToken);
|
|
863
|
+
const validation = handler.validateIdToken(tokens.id_token);
|
|
864
|
+
const claims = validation.claims || {};
|
|
865
|
+
|
|
866
|
+
return {
|
|
867
|
+
sub: claims.sub,
|
|
868
|
+
email: claims.email || null,
|
|
869
|
+
name: claims.name || null,
|
|
870
|
+
groups: claims.groups || [],
|
|
871
|
+
roles: claims.roles || [],
|
|
872
|
+
provider: 'oidc'
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
_audit(event, details) {
|
|
877
|
+
if (!this.auditLog) return;
|
|
878
|
+
this.auditEntries.push({
|
|
879
|
+
timestamp: new Date().toISOString(),
|
|
880
|
+
event,
|
|
881
|
+
details
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// =========================================================================
|
|
887
|
+
// Exports
|
|
888
|
+
// =========================================================================
|
|
889
|
+
|
|
890
|
+
module.exports = {
|
|
891
|
+
SSOManager,
|
|
892
|
+
SAMLParser,
|
|
893
|
+
OIDCHandler,
|
|
894
|
+
IdentityMapper,
|
|
895
|
+
SSOSession,
|
|
896
|
+
DEFAULT_MAPPINGS
|
|
897
|
+
};
|