s3db.js 13.6.0 → 14.0.2
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 +139 -43
- package/dist/s3db.cjs +72425 -38970
- package/dist/s3db.cjs.map +1 -1
- package/dist/s3db.es.js +72177 -38764
- package/dist/s3db.es.js.map +1 -1
- package/mcp/lib/base-handler.js +157 -0
- package/mcp/lib/handlers/connection-handler.js +280 -0
- package/mcp/lib/handlers/query-handler.js +533 -0
- package/mcp/lib/handlers/resource-handler.js +428 -0
- package/mcp/lib/tool-registry.js +336 -0
- package/mcp/lib/tools/connection-tools.js +161 -0
- package/mcp/lib/tools/query-tools.js +267 -0
- package/mcp/lib/tools/resource-tools.js +404 -0
- package/package.json +94 -49
- package/src/clients/memory-client.class.js +346 -191
- package/src/clients/memory-storage.class.js +300 -84
- package/src/clients/s3-client.class.js +7 -6
- package/src/concerns/geo-encoding.js +19 -2
- package/src/concerns/ip.js +59 -9
- package/src/concerns/money.js +8 -1
- package/src/concerns/password-hashing.js +49 -8
- package/src/concerns/plugin-storage.js +186 -18
- package/src/concerns/storage-drivers/filesystem-driver.js +284 -0
- package/src/database.class.js +139 -29
- package/src/errors.js +332 -42
- package/src/plugins/api/auth/oidc-auth.js +66 -17
- package/src/plugins/api/auth/strategies/base-strategy.class.js +74 -0
- package/src/plugins/api/auth/strategies/factory.class.js +63 -0
- package/src/plugins/api/auth/strategies/global-strategy.class.js +44 -0
- package/src/plugins/api/auth/strategies/path-based-strategy.class.js +83 -0
- package/src/plugins/api/auth/strategies/path-rules-strategy.class.js +118 -0
- package/src/plugins/api/concerns/failban-manager.js +106 -57
- package/src/plugins/api/concerns/opengraph-helper.js +116 -0
- package/src/plugins/api/concerns/route-context.js +601 -0
- package/src/plugins/api/concerns/state-machine.js +288 -0
- package/src/plugins/api/index.js +180 -41
- package/src/plugins/api/routes/auth-routes.js +198 -30
- package/src/plugins/api/routes/resource-routes.js +19 -4
- package/src/plugins/api/server/health-manager.class.js +163 -0
- package/src/plugins/api/server/middleware-chain.class.js +310 -0
- package/src/plugins/api/server/router.class.js +472 -0
- package/src/plugins/api/server.js +280 -1303
- package/src/plugins/api/utils/custom-routes.js +17 -5
- package/src/plugins/api/utils/guards.js +76 -17
- package/src/plugins/api/utils/openapi-generator-cached.class.js +133 -0
- package/src/plugins/api/utils/openapi-generator.js +7 -6
- package/src/plugins/api/utils/template-engine.js +77 -3
- package/src/plugins/audit.plugin.js +30 -8
- package/src/plugins/backup.plugin.js +110 -14
- package/src/plugins/cache/cache.class.js +22 -5
- package/src/plugins/cache/filesystem-cache.class.js +116 -19
- package/src/plugins/cache/memory-cache.class.js +211 -57
- package/src/plugins/cache/multi-tier-cache.class.js +371 -0
- package/src/plugins/cache/partition-aware-filesystem-cache.class.js +168 -47
- package/src/plugins/cache/redis-cache.class.js +552 -0
- package/src/plugins/cache/s3-cache.class.js +17 -8
- package/src/plugins/cache.plugin.js +176 -61
- package/src/plugins/cloud-inventory/drivers/alibaba-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/aws-driver.js +60 -29
- package/src/plugins/cloud-inventory/drivers/azure-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/base-driver.js +16 -2
- package/src/plugins/cloud-inventory/drivers/cloudflare-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/digitalocean-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/hetzner-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/linode-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/mongodb-atlas-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/vultr-driver.js +8 -1
- package/src/plugins/cloud-inventory/index.js +29 -8
- package/src/plugins/cloud-inventory/registry.js +64 -42
- package/src/plugins/cloud-inventory.plugin.js +240 -138
- package/src/plugins/concerns/plugin-dependencies.js +54 -0
- package/src/plugins/concerns/resource-names.js +100 -0
- package/src/plugins/consumers/index.js +10 -2
- package/src/plugins/consumers/sqs-consumer.js +12 -2
- package/src/plugins/cookie-farm-suite.plugin.js +278 -0
- package/src/plugins/cookie-farm.errors.js +73 -0
- package/src/plugins/cookie-farm.plugin.js +869 -0
- package/src/plugins/costs.plugin.js +7 -1
- package/src/plugins/eventual-consistency/analytics.js +94 -19
- package/src/plugins/eventual-consistency/config.js +15 -7
- package/src/plugins/eventual-consistency/consolidation.js +29 -11
- package/src/plugins/eventual-consistency/garbage-collection.js +3 -1
- package/src/plugins/eventual-consistency/helpers.js +39 -14
- package/src/plugins/eventual-consistency/install.js +21 -2
- package/src/plugins/eventual-consistency/utils.js +32 -10
- package/src/plugins/fulltext.plugin.js +38 -11
- package/src/plugins/geo.plugin.js +61 -9
- package/src/plugins/identity/concerns/config.js +61 -0
- package/src/plugins/identity/concerns/mfa-manager.js +15 -2
- package/src/plugins/identity/concerns/rate-limit.js +124 -0
- package/src/plugins/identity/concerns/resource-schemas.js +9 -1
- package/src/plugins/identity/concerns/token-generator.js +29 -4
- package/src/plugins/identity/drivers/auth-driver.interface.js +76 -0
- package/src/plugins/identity/drivers/client-credentials-driver.js +127 -0
- package/src/plugins/identity/drivers/index.js +18 -0
- package/src/plugins/identity/drivers/password-driver.js +122 -0
- package/src/plugins/identity/email-service.js +17 -2
- package/src/plugins/identity/index.js +413 -69
- package/src/plugins/identity/oauth2-server.js +413 -30
- package/src/plugins/identity/oidc-discovery.js +16 -8
- package/src/plugins/identity/rsa-keys.js +115 -35
- package/src/plugins/identity/server.js +166 -45
- package/src/plugins/identity/session-manager.js +53 -7
- package/src/plugins/identity/ui/pages/mfa-verification.js +17 -15
- package/src/plugins/identity/ui/routes.js +363 -255
- package/src/plugins/importer/index.js +153 -20
- package/src/plugins/index.js +9 -2
- package/src/plugins/kubernetes-inventory/index.js +6 -0
- package/src/plugins/kubernetes-inventory/k8s-driver.js +867 -0
- package/src/plugins/kubernetes-inventory/resource-types.js +274 -0
- package/src/plugins/kubernetes-inventory.plugin.js +980 -0
- package/src/plugins/metrics.plugin.js +64 -16
- package/src/plugins/ml/base-model.class.js +25 -15
- package/src/plugins/ml/regression-model.class.js +1 -1
- package/src/plugins/ml.errors.js +57 -25
- package/src/plugins/ml.plugin.js +28 -4
- package/src/plugins/namespace.js +210 -0
- package/src/plugins/plugin.class.js +180 -8
- package/src/plugins/puppeteer/console-monitor.js +729 -0
- package/src/plugins/puppeteer/cookie-manager.js +492 -0
- package/src/plugins/puppeteer/network-monitor.js +816 -0
- package/src/plugins/puppeteer/performance-manager.js +746 -0
- package/src/plugins/puppeteer/proxy-manager.js +478 -0
- package/src/plugins/puppeteer/stealth-manager.js +556 -0
- package/src/plugins/puppeteer.errors.js +81 -0
- package/src/plugins/puppeteer.plugin.js +1327 -0
- package/src/plugins/queue-consumer.plugin.js +69 -14
- package/src/plugins/recon/behaviors/uptime-behavior.js +691 -0
- package/src/plugins/recon/concerns/command-runner.js +148 -0
- package/src/plugins/recon/concerns/diff-detector.js +372 -0
- package/src/plugins/recon/concerns/fingerprint-builder.js +307 -0
- package/src/plugins/recon/concerns/process-manager.js +338 -0
- package/src/plugins/recon/concerns/report-generator.js +478 -0
- package/src/plugins/recon/concerns/security-analyzer.js +571 -0
- package/src/plugins/recon/concerns/target-normalizer.js +68 -0
- package/src/plugins/recon/config/defaults.js +321 -0
- package/src/plugins/recon/config/resources.js +370 -0
- package/src/plugins/recon/index.js +778 -0
- package/src/plugins/recon/managers/dependency-manager.js +174 -0
- package/src/plugins/recon/managers/scheduler-manager.js +179 -0
- package/src/plugins/recon/managers/storage-manager.js +745 -0
- package/src/plugins/recon/managers/target-manager.js +274 -0
- package/src/plugins/recon/stages/asn-stage.js +314 -0
- package/src/plugins/recon/stages/certificate-stage.js +84 -0
- package/src/plugins/recon/stages/dns-stage.js +107 -0
- package/src/plugins/recon/stages/dnsdumpster-stage.js +362 -0
- package/src/plugins/recon/stages/fingerprint-stage.js +71 -0
- package/src/plugins/recon/stages/google-dorks-stage.js +440 -0
- package/src/plugins/recon/stages/http-stage.js +89 -0
- package/src/plugins/recon/stages/latency-stage.js +148 -0
- package/src/plugins/recon/stages/massdns-stage.js +302 -0
- package/src/plugins/recon/stages/osint-stage.js +1373 -0
- package/src/plugins/recon/stages/ports-stage.js +169 -0
- package/src/plugins/recon/stages/screenshot-stage.js +94 -0
- package/src/plugins/recon/stages/secrets-stage.js +514 -0
- package/src/plugins/recon/stages/subdomains-stage.js +295 -0
- package/src/plugins/recon/stages/tls-audit-stage.js +78 -0
- package/src/plugins/recon/stages/vulnerability-stage.js +78 -0
- package/src/plugins/recon/stages/web-discovery-stage.js +113 -0
- package/src/plugins/recon/stages/whois-stage.js +349 -0
- package/src/plugins/recon.plugin.js +75 -0
- package/src/plugins/recon.plugin.js.backup +2635 -0
- package/src/plugins/relation.errors.js +87 -14
- package/src/plugins/replicator.plugin.js +514 -137
- package/src/plugins/replicators/base-replicator.class.js +89 -1
- package/src/plugins/replicators/bigquery-replicator.class.js +66 -22
- package/src/plugins/replicators/dynamodb-replicator.class.js +22 -15
- package/src/plugins/replicators/mongodb-replicator.class.js +22 -15
- package/src/plugins/replicators/mysql-replicator.class.js +52 -17
- package/src/plugins/replicators/planetscale-replicator.class.js +30 -4
- package/src/plugins/replicators/postgres-replicator.class.js +62 -27
- package/src/plugins/replicators/s3db-replicator.class.js +25 -18
- package/src/plugins/replicators/schema-sync.helper.js +3 -3
- package/src/plugins/replicators/sqs-replicator.class.js +8 -2
- package/src/plugins/replicators/turso-replicator.class.js +23 -3
- package/src/plugins/replicators/webhook-replicator.class.js +42 -4
- package/src/plugins/s3-queue.plugin.js +464 -65
- package/src/plugins/scheduler.plugin.js +20 -6
- package/src/plugins/state-machine.plugin.js +40 -9
- package/src/plugins/tfstate/README.md +126 -126
- package/src/plugins/tfstate/base-driver.js +28 -4
- package/src/plugins/tfstate/errors.js +65 -10
- package/src/plugins/tfstate/filesystem-driver.js +52 -8
- package/src/plugins/tfstate/index.js +163 -90
- package/src/plugins/tfstate/s3-driver.js +64 -6
- package/src/plugins/ttl.plugin.js +72 -17
- package/src/plugins/vector/distances.js +18 -12
- package/src/plugins/vector/kmeans.js +26 -4
- package/src/resource.class.js +115 -19
- package/src/testing/factory.class.js +20 -3
- package/src/testing/seeder.class.js +7 -1
- package/src/clients/memory-client.md +0 -917
- package/src/plugins/cloud-inventory/drivers/mock-drivers.js +0 -449
|
@@ -40,6 +40,24 @@ import {
|
|
|
40
40
|
generateClientId,
|
|
41
41
|
generateClientSecret
|
|
42
42
|
} from './oidc-discovery.js';
|
|
43
|
+
import tryFn from '../../concerns/try-fn.js';
|
|
44
|
+
import { verifyPassword } from './concerns/password.js';
|
|
45
|
+
import { PluginError } from '../../errors.js';
|
|
46
|
+
|
|
47
|
+
function constantTimeEqual(a, b) {
|
|
48
|
+
const valueA = Buffer.from(String(a ?? ''), 'utf8');
|
|
49
|
+
const valueB = Buffer.from(String(b ?? ''), 'utf8');
|
|
50
|
+
|
|
51
|
+
if (valueA.length !== valueB.length) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let mismatch = 0;
|
|
56
|
+
for (let i = 0; i < valueA.length; i += 1) {
|
|
57
|
+
mismatch |= valueA[i] ^ valueB[i];
|
|
58
|
+
}
|
|
59
|
+
return mismatch === 0;
|
|
60
|
+
}
|
|
43
61
|
|
|
44
62
|
/**
|
|
45
63
|
* OAuth2/OIDC Authorization Server
|
|
@@ -53,7 +71,7 @@ export class OAuth2Server {
|
|
|
53
71
|
clientResource,
|
|
54
72
|
authCodeResource,
|
|
55
73
|
supportedScopes = ['openid', 'profile', 'email', 'offline_access'],
|
|
56
|
-
supportedGrantTypes = ['authorization_code', 'client_credentials', 'refresh_token'],
|
|
74
|
+
supportedGrantTypes = ['authorization_code', 'client_credentials', 'refresh_token', 'password'],
|
|
57
75
|
supportedResponseTypes = ['code', 'token', 'id_token'],
|
|
58
76
|
accessTokenExpiry = '15m',
|
|
59
77
|
idTokenExpiry = '15m',
|
|
@@ -62,15 +80,33 @@ export class OAuth2Server {
|
|
|
62
80
|
} = options;
|
|
63
81
|
|
|
64
82
|
if (!issuer) {
|
|
65
|
-
throw new
|
|
83
|
+
throw new PluginError('Issuer URL is required for OAuth2Server', {
|
|
84
|
+
pluginName: 'IdentityPlugin',
|
|
85
|
+
operation: 'OAuth2Server.constructor',
|
|
86
|
+
statusCode: 400,
|
|
87
|
+
retriable: false,
|
|
88
|
+
suggestion: 'Pass { issuer: "https://auth.example.com" } when initializing OAuth2Server.'
|
|
89
|
+
});
|
|
66
90
|
}
|
|
67
91
|
|
|
68
92
|
if (!keyResource) {
|
|
69
|
-
throw new
|
|
93
|
+
throw new PluginError('keyResource is required for OAuth2Server', {
|
|
94
|
+
pluginName: 'IdentityPlugin',
|
|
95
|
+
operation: 'OAuth2Server.constructor',
|
|
96
|
+
statusCode: 400,
|
|
97
|
+
retriable: false,
|
|
98
|
+
suggestion: 'Provide a keyResource (S3DB resource) to store signing keys.'
|
|
99
|
+
});
|
|
70
100
|
}
|
|
71
101
|
|
|
72
102
|
if (!userResource) {
|
|
73
|
-
throw new
|
|
103
|
+
throw new PluginError('userResource is required for OAuth2Server', {
|
|
104
|
+
pluginName: 'IdentityPlugin',
|
|
105
|
+
operation: 'OAuth2Server.constructor',
|
|
106
|
+
statusCode: 400,
|
|
107
|
+
retriable: false,
|
|
108
|
+
suggestion: 'Provide a userResource to look up user accounts during token exchange.'
|
|
109
|
+
});
|
|
74
110
|
}
|
|
75
111
|
|
|
76
112
|
this.issuer = issuer.replace(/\/$/, '');
|
|
@@ -87,6 +123,7 @@ export class OAuth2Server {
|
|
|
87
123
|
this.authCodeExpiry = authCodeExpiry;
|
|
88
124
|
|
|
89
125
|
this.keyManager = new KeyManager(keyResource);
|
|
126
|
+
this.identityPlugin = null;
|
|
90
127
|
}
|
|
91
128
|
|
|
92
129
|
/**
|
|
@@ -96,6 +133,10 @@ export class OAuth2Server {
|
|
|
96
133
|
await this.keyManager.initialize();
|
|
97
134
|
}
|
|
98
135
|
|
|
136
|
+
setIdentityPlugin(identityPlugin) {
|
|
137
|
+
this.identityPlugin = identityPlugin;
|
|
138
|
+
}
|
|
139
|
+
|
|
99
140
|
/**
|
|
100
141
|
* OIDC Discovery endpoint handler
|
|
101
142
|
* GET /.well-known/openid-configuration
|
|
@@ -170,27 +211,45 @@ export class OAuth2Server {
|
|
|
170
211
|
});
|
|
171
212
|
}
|
|
172
213
|
|
|
173
|
-
|
|
214
|
+
let authenticatedClient = null;
|
|
215
|
+
|
|
174
216
|
if (this.clientResource) {
|
|
175
|
-
|
|
176
|
-
if (!
|
|
217
|
+
authenticatedClient = await this.authenticateClient(client_id, client_secret);
|
|
218
|
+
if (!authenticatedClient) {
|
|
177
219
|
return res.status(401).json({
|
|
178
220
|
error: 'invalid_client',
|
|
179
221
|
error_description: 'Client authentication failed'
|
|
180
222
|
});
|
|
181
223
|
}
|
|
224
|
+
} else if (client_id) {
|
|
225
|
+
authenticatedClient = { clientId: client_id };
|
|
182
226
|
}
|
|
183
227
|
|
|
228
|
+
req.authenticatedClient = authenticatedClient;
|
|
229
|
+
|
|
184
230
|
// Handle different grant types
|
|
185
231
|
switch (grant_type) {
|
|
186
232
|
case 'client_credentials':
|
|
187
|
-
return await this.handleClientCredentials(req, res, {
|
|
233
|
+
return await this.handleClientCredentials(req, res, {
|
|
234
|
+
client: authenticatedClient,
|
|
235
|
+
client_id,
|
|
236
|
+
scope
|
|
237
|
+
});
|
|
188
238
|
|
|
189
239
|
case 'authorization_code':
|
|
190
240
|
return await this.handleAuthorizationCode(req, res);
|
|
191
241
|
|
|
192
242
|
case 'refresh_token':
|
|
193
|
-
return await this.handleRefreshToken(req, res
|
|
243
|
+
return await this.handleRefreshToken(req, res, {
|
|
244
|
+
client: authenticatedClient,
|
|
245
|
+
scope
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
case 'password':
|
|
249
|
+
return await this.handlePasswordGrant(req, res, {
|
|
250
|
+
client: authenticatedClient,
|
|
251
|
+
scope
|
|
252
|
+
});
|
|
194
253
|
|
|
195
254
|
default:
|
|
196
255
|
return res.status(400).json({
|
|
@@ -209,7 +268,30 @@ export class OAuth2Server {
|
|
|
209
268
|
/**
|
|
210
269
|
* Client Credentials flow handler
|
|
211
270
|
*/
|
|
212
|
-
async handleClientCredentials(req, res, { client_id, scope }) {
|
|
271
|
+
async handleClientCredentials(req, res, { client, client_id, scope } = {}) {
|
|
272
|
+
const resolvedClient = client || (client_id ? { clientId: client_id } : null);
|
|
273
|
+
const resolvedClientId = resolvedClient?.clientId || client_id;
|
|
274
|
+
|
|
275
|
+
if (!resolvedClientId) {
|
|
276
|
+
return res.status(400).json({
|
|
277
|
+
error: 'invalid_request',
|
|
278
|
+
error_description: 'client_id is required'
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const allowedGrantTypes = Array.isArray(resolvedClient?.grantTypes)
|
|
283
|
+
? resolvedClient.grantTypes
|
|
284
|
+
: Array.isArray(resolvedClient?.allowedGrantTypes)
|
|
285
|
+
? resolvedClient.allowedGrantTypes
|
|
286
|
+
: null;
|
|
287
|
+
|
|
288
|
+
if (allowedGrantTypes && allowedGrantTypes.length > 0 && !allowedGrantTypes.includes('client_credentials')) {
|
|
289
|
+
return res.status(400).json({
|
|
290
|
+
error: 'unauthorized_client',
|
|
291
|
+
error_description: 'Client is not allowed to use client_credentials grant'
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
213
295
|
const scopes = parseScopes(scope);
|
|
214
296
|
|
|
215
297
|
// Validate scopes
|
|
@@ -221,10 +303,20 @@ export class OAuth2Server {
|
|
|
221
303
|
});
|
|
222
304
|
}
|
|
223
305
|
|
|
306
|
+
if (Array.isArray(resolvedClient?.allowedScopes) && resolvedClient.allowedScopes.length > 0) {
|
|
307
|
+
const disallowedScopes = scopeValidation.scopes.filter(scopeValue => !resolvedClient.allowedScopes.includes(scopeValue));
|
|
308
|
+
if (disallowedScopes.length > 0) {
|
|
309
|
+
return res.status(400).json({
|
|
310
|
+
error: 'invalid_scope',
|
|
311
|
+
error_description: `Client is not allowed to request scopes: ${disallowedScopes.join(', ')}`
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
224
316
|
// Create access token
|
|
225
317
|
const accessToken = this.keyManager.createToken({
|
|
226
318
|
iss: this.issuer,
|
|
227
|
-
sub:
|
|
319
|
+
sub: resolvedClientId,
|
|
228
320
|
aud: this.issuer,
|
|
229
321
|
scope: scopeValidation.scopes.join(' '),
|
|
230
322
|
token_type: 'access_token'
|
|
@@ -271,8 +363,16 @@ export class OAuth2Server {
|
|
|
271
363
|
const authCode = authCodes[0];
|
|
272
364
|
|
|
273
365
|
// Validate code expiration
|
|
274
|
-
const
|
|
275
|
-
if (
|
|
366
|
+
const expiresAtMs = this.parseAuthCodeExpiry(authCode.expiresAt);
|
|
367
|
+
if (!Number.isFinite(expiresAtMs)) {
|
|
368
|
+
await this.authCodeResource.remove(authCode.id);
|
|
369
|
+
return res.status(400).json({
|
|
370
|
+
error: 'invalid_grant',
|
|
371
|
+
error_description: 'Authorization code is invalid'
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (expiresAtMs <= Date.now()) {
|
|
276
376
|
await this.authCodeResource.remove(authCode.id);
|
|
277
377
|
return res.status(400).json({
|
|
278
378
|
error: 'invalid_grant',
|
|
@@ -372,11 +472,148 @@ export class OAuth2Server {
|
|
|
372
472
|
return res.status(200).json(response);
|
|
373
473
|
}
|
|
374
474
|
|
|
475
|
+
async handlePasswordGrant(req, res, context = {}) {
|
|
476
|
+
if (!this.identityPlugin || typeof this.identityPlugin.authenticateWithPassword !== 'function') {
|
|
477
|
+
return res.status(400).json({
|
|
478
|
+
error: 'unsupported_grant_type',
|
|
479
|
+
error_description: 'Password grant is not configured'
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const driver = this.identityPlugin.getAuthDriver?.('password');
|
|
484
|
+
if (!driver || (typeof driver.supportsGrant === 'function' && !driver.supportsGrant('password'))) {
|
|
485
|
+
return res.status(400).json({
|
|
486
|
+
error: 'unsupported_grant_type',
|
|
487
|
+
error_description: 'Password grant is not available'
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const client = context.client ?? req.authenticatedClient ?? null;
|
|
492
|
+
|
|
493
|
+
if (client) {
|
|
494
|
+
if (client.active === false) {
|
|
495
|
+
return res.status(400).json({
|
|
496
|
+
error: 'unauthorized_client',
|
|
497
|
+
error_description: 'Client is inactive'
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const allowedGrantTypes = Array.isArray(client.grantTypes)
|
|
502
|
+
? client.grantTypes
|
|
503
|
+
: Array.isArray(client.allowedGrantTypes)
|
|
504
|
+
? client.allowedGrantTypes
|
|
505
|
+
: null;
|
|
506
|
+
|
|
507
|
+
if (allowedGrantTypes && allowedGrantTypes.length > 0 && !allowedGrantTypes.includes('password')) {
|
|
508
|
+
return res.status(400).json({
|
|
509
|
+
error: 'unauthorized_client',
|
|
510
|
+
error_description: 'Client is not allowed to use password grant'
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const { username, password } = req.body;
|
|
516
|
+
if (!username || !password) {
|
|
517
|
+
return res.status(400).json({
|
|
518
|
+
error: 'invalid_request',
|
|
519
|
+
error_description: 'username and password are required'
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const authResult = await this.identityPlugin.authenticateWithPassword({
|
|
524
|
+
email: username,
|
|
525
|
+
password
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
if (!authResult?.success || !authResult.user) {
|
|
529
|
+
return res.status(authResult?.statusCode || 400).json({
|
|
530
|
+
error: authResult?.error || 'invalid_grant',
|
|
531
|
+
error_description: 'Invalid credentials'
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const user = authResult.user;
|
|
536
|
+
if (user.active !== undefined && user.active === false) {
|
|
537
|
+
return res.status(400).json({
|
|
538
|
+
error: 'invalid_grant',
|
|
539
|
+
error_description: 'User account is inactive'
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const requestedScope = context.scope ?? req.body.scope;
|
|
544
|
+
const scopes = parseScopes(requestedScope);
|
|
545
|
+
const scopeValidation = validateScopes(scopes, this.supportedScopes);
|
|
546
|
+
if (!scopeValidation.valid) {
|
|
547
|
+
return res.status(400).json({
|
|
548
|
+
error: 'invalid_scope',
|
|
549
|
+
error_description: scopeValidation.error
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (client && Array.isArray(client.allowedScopes) && client.allowedScopes.length > 0) {
|
|
554
|
+
const disallowedScopes = scopeValidation.scopes.filter(scopeValue => !client.allowedScopes.includes(scopeValue));
|
|
555
|
+
if (disallowedScopes.length > 0) {
|
|
556
|
+
return res.status(400).json({
|
|
557
|
+
error: 'invalid_scope',
|
|
558
|
+
error_description: `Client is not allowed to request scopes: ${disallowedScopes.join(', ')}`
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const resolvedAudience = client?.clientId || req.body.client_id || this.issuer;
|
|
564
|
+
|
|
565
|
+
const accessToken = this.keyManager.createToken({
|
|
566
|
+
iss: this.issuer,
|
|
567
|
+
sub: user.id,
|
|
568
|
+
aud: resolvedAudience,
|
|
569
|
+
scope: scopeValidation.scopes.join(' '),
|
|
570
|
+
token_type: 'access_token'
|
|
571
|
+
}, this.accessTokenExpiry);
|
|
572
|
+
|
|
573
|
+
const response = {
|
|
574
|
+
access_token: accessToken,
|
|
575
|
+
token_type: 'Bearer',
|
|
576
|
+
expires_in: this.parseExpiryToSeconds(this.accessTokenExpiry),
|
|
577
|
+
scope: scopeValidation.scopes.join(' ')
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
const allowRefreshToken = scopeValidation.scopes.includes('offline_access') &&
|
|
581
|
+
this.supportedGrantTypes.includes('refresh_token');
|
|
582
|
+
|
|
583
|
+
if (allowRefreshToken) {
|
|
584
|
+
const refreshToken = this.keyManager.createToken({
|
|
585
|
+
iss: this.issuer,
|
|
586
|
+
sub: user.id,
|
|
587
|
+
aud: this.issuer,
|
|
588
|
+
scope: scopeValidation.scopes.join(' '),
|
|
589
|
+
token_type: 'refresh_token'
|
|
590
|
+
}, this.refreshTokenExpiry);
|
|
591
|
+
|
|
592
|
+
response.refresh_token = refreshToken;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if (scopeValidation.scopes.includes('openid')) {
|
|
596
|
+
const userClaims = extractUserClaims(user, scopeValidation.scopes);
|
|
597
|
+
|
|
598
|
+
const idToken = this.keyManager.createToken({
|
|
599
|
+
iss: this.issuer,
|
|
600
|
+
sub: user.id,
|
|
601
|
+
aud: resolvedAudience,
|
|
602
|
+
...userClaims
|
|
603
|
+
}, this.idTokenExpiry);
|
|
604
|
+
|
|
605
|
+
response.id_token = idToken;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
return res.status(200).json(response);
|
|
609
|
+
}
|
|
610
|
+
|
|
375
611
|
/**
|
|
376
612
|
* Refresh Token flow handler
|
|
377
613
|
*/
|
|
378
|
-
async handleRefreshToken(req, res) {
|
|
614
|
+
async handleRefreshToken(req, res, context = {}) {
|
|
379
615
|
const { refresh_token, scope } = req.body;
|
|
616
|
+
const client = context.client ?? req.authenticatedClient ?? null;
|
|
380
617
|
|
|
381
618
|
if (!refresh_token) {
|
|
382
619
|
return res.status(400).json({
|
|
@@ -405,6 +642,33 @@ export class OAuth2Server {
|
|
|
405
642
|
});
|
|
406
643
|
}
|
|
407
644
|
|
|
645
|
+
if (client?.clientId && payload.aud && payload.aud !== client.clientId) {
|
|
646
|
+
return res.status(400).json({
|
|
647
|
+
error: 'invalid_grant',
|
|
648
|
+
error_description: 'Refresh token does not belong to this client'
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
if (client?.active === false) {
|
|
653
|
+
return res.status(400).json({
|
|
654
|
+
error: 'unauthorized_client',
|
|
655
|
+
error_description: 'Client is inactive'
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const allowedGrantTypes = Array.isArray(client?.grantTypes)
|
|
660
|
+
? client.grantTypes
|
|
661
|
+
: Array.isArray(client?.allowedGrantTypes)
|
|
662
|
+
? client.allowedGrantTypes
|
|
663
|
+
: null;
|
|
664
|
+
|
|
665
|
+
if (allowedGrantTypes && allowedGrantTypes.length > 0 && !allowedGrantTypes.includes('refresh_token')) {
|
|
666
|
+
return res.status(400).json({
|
|
667
|
+
error: 'unauthorized_client',
|
|
668
|
+
error_description: 'Client is not allowed to use refresh_token grant'
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
|
|
408
672
|
// Validate claims
|
|
409
673
|
const claimValidation = validateClaims(payload, {
|
|
410
674
|
issuer: this.issuer,
|
|
@@ -419,7 +683,8 @@ export class OAuth2Server {
|
|
|
419
683
|
}
|
|
420
684
|
|
|
421
685
|
// Parse scopes (use original scopes if not provided)
|
|
422
|
-
const
|
|
686
|
+
const overrideScope = context.scope ?? scope;
|
|
687
|
+
const requestedScopes = overrideScope ? parseScopes(overrideScope) : parseScopes(payload.scope);
|
|
423
688
|
const originalScopes = parseScopes(payload.scope);
|
|
424
689
|
|
|
425
690
|
// Requested scopes must be subset of original scopes
|
|
@@ -431,6 +696,16 @@ export class OAuth2Server {
|
|
|
431
696
|
});
|
|
432
697
|
}
|
|
433
698
|
|
|
699
|
+
if (client && Array.isArray(client.allowedScopes) && client.allowedScopes.length > 0) {
|
|
700
|
+
const disallowedScopes = requestedScopes.filter(scopeValue => !client.allowedScopes.includes(scopeValue));
|
|
701
|
+
if (disallowedScopes.length > 0) {
|
|
702
|
+
return res.status(400).json({
|
|
703
|
+
error: 'invalid_scope',
|
|
704
|
+
error_description: `Client is not allowed to request scopes: ${disallowedScopes.join(', ')}`
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
434
709
|
// Get user
|
|
435
710
|
const user = await this.userResource.get(payload.sub);
|
|
436
711
|
if (!user) {
|
|
@@ -452,7 +727,8 @@ export class OAuth2Server {
|
|
|
452
727
|
const response = {
|
|
453
728
|
access_token: accessToken,
|
|
454
729
|
token_type: 'Bearer',
|
|
455
|
-
expires_in: this.parseExpiryToSeconds(this.accessTokenExpiry)
|
|
730
|
+
expires_in: this.parseExpiryToSeconds(this.accessTokenExpiry),
|
|
731
|
+
scope: requestedScopes.join(' ')
|
|
456
732
|
};
|
|
457
733
|
|
|
458
734
|
// Create new ID token if openid scope requested
|
|
@@ -601,24 +877,96 @@ export class OAuth2Server {
|
|
|
601
877
|
return null;
|
|
602
878
|
}
|
|
603
879
|
|
|
604
|
-
|
|
605
|
-
|
|
880
|
+
const driver = this.identityPlugin?.getAuthDriver?.('client_credentials');
|
|
881
|
+
if (driver) {
|
|
882
|
+
const authResult = await driver.authenticate({ clientId, clientSecret });
|
|
883
|
+
if (authResult?.success) {
|
|
884
|
+
return authResult.client || { clientId };
|
|
885
|
+
}
|
|
886
|
+
return null;
|
|
887
|
+
}
|
|
606
888
|
|
|
607
|
-
|
|
608
|
-
|
|
889
|
+
const [ok, err, clients] = await tryFn(() => this.clientResource.query({ clientId }));
|
|
890
|
+
if (!ok) {
|
|
891
|
+
if (err && this.identityPlugin?.config?.verbose) {
|
|
892
|
+
console.error('[Identity Plugin] Failed to query clients resource:', err.message);
|
|
609
893
|
}
|
|
894
|
+
return null;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
if (!clients || clients.length === 0) {
|
|
898
|
+
return null;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
const client = clients[0];
|
|
610
902
|
|
|
611
|
-
|
|
903
|
+
if (client.active === false) {
|
|
904
|
+
return null;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
const secrets = [];
|
|
908
|
+
if (Array.isArray(client.secrets)) {
|
|
909
|
+
secrets.push(...client.secrets);
|
|
910
|
+
}
|
|
911
|
+
if (client.clientSecret) {
|
|
912
|
+
secrets.push(client.clientSecret);
|
|
913
|
+
}
|
|
914
|
+
if (client.secret) {
|
|
915
|
+
secrets.push(client.secret);
|
|
916
|
+
}
|
|
612
917
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
918
|
+
if (!secrets.length) {
|
|
919
|
+
return null;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
let secretMatches = false;
|
|
923
|
+
for (const storedSecret of secrets) {
|
|
924
|
+
if (!storedSecret) continue;
|
|
925
|
+
|
|
926
|
+
if (this._isHashedSecret(storedSecret)) {
|
|
927
|
+
try {
|
|
928
|
+
const okHash = await verifyPassword(clientSecret, storedSecret);
|
|
929
|
+
if (okHash) {
|
|
930
|
+
secretMatches = true;
|
|
931
|
+
break;
|
|
932
|
+
}
|
|
933
|
+
} catch (error) {
|
|
934
|
+
if (this.identityPlugin?.config?.verbose) {
|
|
935
|
+
console.error('[Identity Plugin] Failed to verify client secret hash:', error.message);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
continue;
|
|
616
939
|
}
|
|
617
940
|
|
|
618
|
-
|
|
619
|
-
|
|
941
|
+
if (constantTimeEqual(clientSecret, storedSecret)) {
|
|
942
|
+
secretMatches = true;
|
|
943
|
+
break;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
if (!secretMatches) {
|
|
620
948
|
return null;
|
|
621
949
|
}
|
|
950
|
+
|
|
951
|
+
const sanitizedClient = { ...client };
|
|
952
|
+
if (sanitizedClient.clientSecret !== undefined) {
|
|
953
|
+
delete sanitizedClient.clientSecret;
|
|
954
|
+
}
|
|
955
|
+
if (sanitizedClient.secret !== undefined) {
|
|
956
|
+
delete sanitizedClient.secret;
|
|
957
|
+
}
|
|
958
|
+
if (sanitizedClient.secrets !== undefined) {
|
|
959
|
+
delete sanitizedClient.secrets;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
return sanitizedClient;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
_isHashedSecret(value) {
|
|
966
|
+
if (typeof value !== 'string') {
|
|
967
|
+
return false;
|
|
968
|
+
}
|
|
969
|
+
return value.startsWith('$') || value.startsWith('s3db$');
|
|
622
970
|
}
|
|
623
971
|
|
|
624
972
|
/**
|
|
@@ -646,7 +994,13 @@ export class OAuth2Server {
|
|
|
646
994
|
parseExpiryToSeconds(expiresIn) {
|
|
647
995
|
const match = expiresIn.match(/^(\d+)([smhd])$/);
|
|
648
996
|
if (!match) {
|
|
649
|
-
throw new
|
|
997
|
+
throw new PluginError('Invalid expiresIn format', {
|
|
998
|
+
pluginName: 'IdentityPlugin',
|
|
999
|
+
operation: 'parseExpiryToSeconds',
|
|
1000
|
+
statusCode: 400,
|
|
1001
|
+
retriable: false,
|
|
1002
|
+
suggestion: 'Use a duration string such as "15m", "24h", or "30s".'
|
|
1003
|
+
});
|
|
650
1004
|
}
|
|
651
1005
|
|
|
652
1006
|
const [, value, unit] = match;
|
|
@@ -812,9 +1166,19 @@ export class OAuth2Server {
|
|
|
812
1166
|
|
|
813
1167
|
const user = users[0];
|
|
814
1168
|
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
1169
|
+
const [okVerify, errVerify, isValid] = await tryFn(() =>
|
|
1170
|
+
verifyPassword(password, user.password)
|
|
1171
|
+
);
|
|
1172
|
+
|
|
1173
|
+
if (!okVerify) {
|
|
1174
|
+
console.error('[OAuth2Server] Password verification error:', errVerify);
|
|
1175
|
+
return res.status(500).json({
|
|
1176
|
+
error: 'server_error',
|
|
1177
|
+
error_description: 'Authentication failed'
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
if (!isValid) {
|
|
818
1182
|
return res.status(401).json({
|
|
819
1183
|
error: 'access_denied',
|
|
820
1184
|
error_description: 'Invalid credentials'
|
|
@@ -1028,6 +1392,25 @@ export class OAuth2Server {
|
|
|
1028
1392
|
async rotateKeys() {
|
|
1029
1393
|
return await this.keyManager.rotateKey();
|
|
1030
1394
|
}
|
|
1395
|
+
|
|
1396
|
+
/**
|
|
1397
|
+
* Normalize authorization code expiry representation
|
|
1398
|
+
* @param {string|number} value
|
|
1399
|
+
* @returns {number} Expiry timestamp in milliseconds or NaN
|
|
1400
|
+
*/
|
|
1401
|
+
parseAuthCodeExpiry(value) {
|
|
1402
|
+
if (typeof value === 'number') {
|
|
1403
|
+
// If stored as seconds, convert to milliseconds
|
|
1404
|
+
return value > 1e12 ? value : value * 1000;
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
if (typeof value === 'string') {
|
|
1408
|
+
const parsed = Date.parse(value);
|
|
1409
|
+
return Number.isNaN(parsed) ? NaN : parsed;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
return NaN;
|
|
1413
|
+
}
|
|
1031
1414
|
}
|
|
1032
1415
|
|
|
1033
1416
|
export default OAuth2Server;
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* Implements OpenID Connect Discovery 1.0 specification
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { PluginError } from '../../errors.js';
|
|
9
|
+
|
|
8
10
|
/**
|
|
9
11
|
* Generate OpenID Connect Discovery Document
|
|
10
12
|
* @param {Object} options - Configuration options
|
|
@@ -23,7 +25,13 @@ export function generateDiscoveryDocument(options = {}) {
|
|
|
23
25
|
} = options;
|
|
24
26
|
|
|
25
27
|
if (!issuer) {
|
|
26
|
-
throw new
|
|
28
|
+
throw new PluginError('Issuer URL is required for OIDC discovery', {
|
|
29
|
+
pluginName: 'IdentityPlugin',
|
|
30
|
+
operation: 'generateDiscoveryDocument',
|
|
31
|
+
statusCode: 400,
|
|
32
|
+
retriable: false,
|
|
33
|
+
suggestion: 'Provide options.issuer when generating the discovery document.'
|
|
34
|
+
});
|
|
27
35
|
}
|
|
28
36
|
|
|
29
37
|
// Remove trailing slash from issuer
|
|
@@ -31,14 +39,14 @@ export function generateDiscoveryDocument(options = {}) {
|
|
|
31
39
|
|
|
32
40
|
return {
|
|
33
41
|
issuer: baseUrl,
|
|
34
|
-
authorization_endpoint: `${baseUrl}/
|
|
35
|
-
token_endpoint: `${baseUrl}/
|
|
36
|
-
userinfo_endpoint: `${baseUrl}/
|
|
42
|
+
authorization_endpoint: `${baseUrl}/oauth/authorize`,
|
|
43
|
+
token_endpoint: `${baseUrl}/oauth/token`,
|
|
44
|
+
userinfo_endpoint: `${baseUrl}/oauth/userinfo`,
|
|
37
45
|
jwks_uri: `${baseUrl}/.well-known/jwks.json`,
|
|
38
|
-
registration_endpoint: `${baseUrl}/
|
|
39
|
-
introspection_endpoint: `${baseUrl}/
|
|
40
|
-
revocation_endpoint: `${baseUrl}/
|
|
41
|
-
end_session_endpoint: `${baseUrl}/
|
|
46
|
+
registration_endpoint: `${baseUrl}/oauth/register`,
|
|
47
|
+
introspection_endpoint: `${baseUrl}/oauth/introspect`,
|
|
48
|
+
revocation_endpoint: `${baseUrl}/oauth/revoke`,
|
|
49
|
+
end_session_endpoint: `${baseUrl}/logout`,
|
|
42
50
|
|
|
43
51
|
// Supported features
|
|
44
52
|
scopes_supported: scopes,
|