s3db.js 13.4.0 → 13.6.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 +25 -10
- package/dist/{s3db.cjs.js → s3db.cjs} +38801 -32446
- package/dist/s3db.cjs.map +1 -0
- package/dist/s3db.es.js +38653 -32291
- package/dist/s3db.es.js.map +1 -1
- package/package.json +218 -22
- package/src/concerns/id.js +90 -6
- package/src/concerns/index.js +2 -1
- package/src/concerns/password-hashing.js +150 -0
- package/src/database.class.js +6 -2
- package/src/plugins/api/auth/basic-auth.js +40 -10
- package/src/plugins/api/auth/index.js +49 -3
- package/src/plugins/api/auth/oauth2-auth.js +171 -0
- package/src/plugins/api/auth/oidc-auth.js +789 -0
- package/src/plugins/api/auth/oidc-client.js +462 -0
- package/src/plugins/api/auth/path-auth-matcher.js +284 -0
- package/src/plugins/api/concerns/event-emitter.js +134 -0
- package/src/plugins/api/concerns/failban-manager.js +651 -0
- package/src/plugins/api/concerns/guards-helpers.js +402 -0
- package/src/plugins/api/concerns/metrics-collector.js +346 -0
- package/src/plugins/api/index.js +510 -57
- package/src/plugins/api/middlewares/failban.js +305 -0
- package/src/plugins/api/middlewares/rate-limit.js +301 -0
- package/src/plugins/api/middlewares/request-id.js +74 -0
- package/src/plugins/api/middlewares/security-headers.js +120 -0
- package/src/plugins/api/middlewares/session-tracking.js +194 -0
- package/src/plugins/api/routes/auth-routes.js +119 -78
- package/src/plugins/api/routes/resource-routes.js +73 -30
- package/src/plugins/api/server.js +1139 -45
- package/src/plugins/api/utils/custom-routes.js +102 -0
- package/src/plugins/api/utils/guards.js +213 -0
- package/src/plugins/api/utils/mime-types.js +154 -0
- package/src/plugins/api/utils/openapi-generator.js +91 -12
- package/src/plugins/api/utils/path-matcher.js +173 -0
- package/src/plugins/api/utils/static-filesystem.js +262 -0
- package/src/plugins/api/utils/static-s3.js +231 -0
- package/src/plugins/api/utils/template-engine.js +188 -0
- package/src/plugins/cloud-inventory/drivers/alibaba-driver.js +853 -0
- package/src/plugins/cloud-inventory/drivers/aws-driver.js +2554 -0
- package/src/plugins/cloud-inventory/drivers/azure-driver.js +637 -0
- package/src/plugins/cloud-inventory/drivers/base-driver.js +99 -0
- package/src/plugins/cloud-inventory/drivers/cloudflare-driver.js +620 -0
- package/src/plugins/cloud-inventory/drivers/digitalocean-driver.js +698 -0
- package/src/plugins/cloud-inventory/drivers/gcp-driver.js +645 -0
- package/src/plugins/cloud-inventory/drivers/hetzner-driver.js +559 -0
- package/src/plugins/cloud-inventory/drivers/linode-driver.js +614 -0
- package/src/plugins/cloud-inventory/drivers/mock-drivers.js +449 -0
- package/src/plugins/cloud-inventory/drivers/mongodb-atlas-driver.js +771 -0
- package/src/plugins/cloud-inventory/drivers/oracle-driver.js +768 -0
- package/src/plugins/cloud-inventory/drivers/vultr-driver.js +636 -0
- package/src/plugins/cloud-inventory/index.js +20 -0
- package/src/plugins/cloud-inventory/registry.js +146 -0
- package/src/plugins/cloud-inventory/terraform-exporter.js +362 -0
- package/src/plugins/cloud-inventory.plugin.js +1333 -0
- package/src/plugins/concerns/plugin-dependencies.js +62 -2
- package/src/plugins/eventual-consistency/analytics.js +1 -0
- package/src/plugins/eventual-consistency/consolidation.js +2 -2
- package/src/plugins/eventual-consistency/garbage-collection.js +2 -2
- package/src/plugins/eventual-consistency/install.js +2 -2
- package/src/plugins/identity/README.md +335 -0
- package/src/plugins/identity/concerns/mfa-manager.js +204 -0
- package/src/plugins/identity/concerns/password.js +138 -0
- package/src/plugins/identity/concerns/resource-schemas.js +273 -0
- package/src/plugins/identity/concerns/token-generator.js +172 -0
- package/src/plugins/identity/email-service.js +422 -0
- package/src/plugins/identity/index.js +1052 -0
- package/src/plugins/identity/oauth2-server.js +1033 -0
- package/src/plugins/identity/oidc-discovery.js +285 -0
- package/src/plugins/identity/rsa-keys.js +323 -0
- package/src/plugins/identity/server.js +500 -0
- package/src/plugins/identity/session-manager.js +453 -0
- package/src/plugins/identity/ui/layouts/base.js +251 -0
- package/src/plugins/identity/ui/middleware.js +135 -0
- package/src/plugins/identity/ui/pages/admin/client-form.js +247 -0
- package/src/plugins/identity/ui/pages/admin/clients.js +179 -0
- package/src/plugins/identity/ui/pages/admin/dashboard.js +181 -0
- package/src/plugins/identity/ui/pages/admin/user-form.js +283 -0
- package/src/plugins/identity/ui/pages/admin/users.js +263 -0
- package/src/plugins/identity/ui/pages/consent.js +262 -0
- package/src/plugins/identity/ui/pages/forgot-password.js +104 -0
- package/src/plugins/identity/ui/pages/login.js +144 -0
- package/src/plugins/identity/ui/pages/mfa-backup-codes.js +180 -0
- package/src/plugins/identity/ui/pages/mfa-enrollment.js +187 -0
- package/src/plugins/identity/ui/pages/mfa-verification.js +178 -0
- package/src/plugins/identity/ui/pages/oauth-error.js +225 -0
- package/src/plugins/identity/ui/pages/profile.js +361 -0
- package/src/plugins/identity/ui/pages/register.js +226 -0
- package/src/plugins/identity/ui/pages/reset-password.js +128 -0
- package/src/plugins/identity/ui/pages/verify-email.js +172 -0
- package/src/plugins/identity/ui/routes.js +2541 -0
- package/src/plugins/identity/ui/styles/main.css +465 -0
- package/src/plugins/index.js +4 -1
- package/src/plugins/ml/base-model.class.js +65 -16
- package/src/plugins/ml/classification-model.class.js +1 -1
- package/src/plugins/ml/timeseries-model.class.js +3 -1
- package/src/plugins/ml.plugin.js +584 -31
- package/src/plugins/shared/error-handler.js +147 -0
- package/src/plugins/shared/index.js +9 -0
- package/src/plugins/shared/middlewares/compression.js +117 -0
- package/src/plugins/shared/middlewares/cors.js +49 -0
- package/src/plugins/shared/middlewares/index.js +11 -0
- package/src/plugins/shared/middlewares/logging.js +54 -0
- package/src/plugins/shared/middlewares/rate-limit.js +73 -0
- package/src/plugins/shared/middlewares/security.js +158 -0
- package/src/plugins/shared/response-formatter.js +264 -0
- package/src/plugins/state-machine.plugin.js +57 -2
- package/src/resource.class.js +140 -12
- package/src/schema.class.js +30 -1
- package/src/validator.class.js +57 -6
- package/dist/s3db.cjs.js.map +0 -1
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OIDC Client Middleware for Resource Servers
|
|
3
|
+
*
|
|
4
|
+
* Validates RS256 JWT tokens issued by an OAuth2/OIDC Authorization Server.
|
|
5
|
+
* Fetches and caches JWKS (public keys) from the issuer's /.well-known/jwks.json endpoint.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* import { OIDCClient } from 's3db.js/plugins/api/auth/oidc-client';
|
|
9
|
+
*
|
|
10
|
+
* const oidcClient = new OIDCClient({
|
|
11
|
+
* issuer: 'https://sso.example.com',
|
|
12
|
+
* audience: 'https://api.example.com',
|
|
13
|
+
* jwksCacheTTL: 3600000 // 1 hour
|
|
14
|
+
* });
|
|
15
|
+
*
|
|
16
|
+
* await oidcClient.initialize();
|
|
17
|
+
*
|
|
18
|
+
* // Use with API plugin
|
|
19
|
+
* apiPlugin.addAuthDriver('oidc', oidcClient.middleware.bind(oidcClient));
|
|
20
|
+
*
|
|
21
|
+
* // Or use directly in routes
|
|
22
|
+
* apiPlugin.addRoute({
|
|
23
|
+
* path: '/protected',
|
|
24
|
+
* method: 'GET',
|
|
25
|
+
* handler: async (req, res) => {
|
|
26
|
+
* // req.user contains validated token payload
|
|
27
|
+
* res.json({ user: req.user });
|
|
28
|
+
* },
|
|
29
|
+
* auth: 'oidc'
|
|
30
|
+
* });
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { createVerify, createPublicKey } from 'crypto';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Validate JWT claims
|
|
37
|
+
* @param {Object} payload - Token payload
|
|
38
|
+
* @param {Object} options - Validation options
|
|
39
|
+
* @returns {Object} Validation result
|
|
40
|
+
*/
|
|
41
|
+
function validateClaims(payload, options = {}) {
|
|
42
|
+
const {
|
|
43
|
+
issuer,
|
|
44
|
+
audience,
|
|
45
|
+
clockTolerance = 60
|
|
46
|
+
} = options;
|
|
47
|
+
|
|
48
|
+
const now = Math.floor(Date.now() / 1000);
|
|
49
|
+
|
|
50
|
+
// Check required claims
|
|
51
|
+
if (!payload.sub) {
|
|
52
|
+
return { valid: false, error: 'Missing required claim: sub' };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!payload.iat) {
|
|
56
|
+
return { valid: false, error: 'Missing required claim: iat' };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!payload.exp) {
|
|
60
|
+
return { valid: false, error: 'Missing required claim: exp' };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Validate issuer
|
|
64
|
+
if (issuer && payload.iss !== issuer) {
|
|
65
|
+
return {
|
|
66
|
+
valid: false,
|
|
67
|
+
error: `Invalid issuer. Expected: ${issuer}, Got: ${payload.iss}`
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Validate audience
|
|
72
|
+
if (audience) {
|
|
73
|
+
const audiences = Array.isArray(payload.aud) ? payload.aud : [payload.aud];
|
|
74
|
+
|
|
75
|
+
if (!audiences.includes(audience)) {
|
|
76
|
+
return {
|
|
77
|
+
valid: false,
|
|
78
|
+
error: `Invalid audience. Expected: ${audience}, Got: ${audiences.join(', ')}`
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Validate expiration with clock tolerance
|
|
84
|
+
if (payload.exp < (now - clockTolerance)) {
|
|
85
|
+
return { valid: false, error: 'Token has expired' };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Validate not before (if present)
|
|
89
|
+
if (payload.nbf && payload.nbf > (now + clockTolerance)) {
|
|
90
|
+
return { valid: false, error: 'Token not yet valid (nbf)' };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Validate issued at (basic sanity check - not in future)
|
|
94
|
+
if (payload.iat > (now + clockTolerance)) {
|
|
95
|
+
return { valid: false, error: 'Token issued in the future' };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { valid: true, error: null };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* OIDC Client for validating tokens from Authorization Server
|
|
103
|
+
*/
|
|
104
|
+
export class OIDCClient {
|
|
105
|
+
constructor(options = {}) {
|
|
106
|
+
const {
|
|
107
|
+
issuer,
|
|
108
|
+
audience,
|
|
109
|
+
jwksUri,
|
|
110
|
+
jwksCacheTTL = 3600000, // 1 hour
|
|
111
|
+
clockTolerance = 60,
|
|
112
|
+
autoRefreshJWKS = true,
|
|
113
|
+
discoveryUri
|
|
114
|
+
} = options;
|
|
115
|
+
|
|
116
|
+
if (!issuer) {
|
|
117
|
+
throw new Error('issuer is required for OIDCClient');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
this.issuer = issuer.replace(/\/$/, '');
|
|
121
|
+
this.audience = audience;
|
|
122
|
+
this.jwksUri = jwksUri || `${this.issuer}/.well-known/jwks.json`;
|
|
123
|
+
this.discoveryUri = discoveryUri || `${this.issuer}/.well-known/openid-configuration`;
|
|
124
|
+
this.jwksCacheTTL = jwksCacheTTL;
|
|
125
|
+
this.clockTolerance = clockTolerance;
|
|
126
|
+
this.autoRefreshJWKS = autoRefreshJWKS;
|
|
127
|
+
|
|
128
|
+
this.jwksCache = null;
|
|
129
|
+
this.jwksCacheExpiry = null;
|
|
130
|
+
this.discoveryCache = null;
|
|
131
|
+
this.keys = new Map(); // kid → publicKey (PEM)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Initialize OIDC client - fetch discovery document and JWKS
|
|
136
|
+
*/
|
|
137
|
+
async initialize() {
|
|
138
|
+
await this.fetchDiscovery();
|
|
139
|
+
await this.fetchJWKS();
|
|
140
|
+
|
|
141
|
+
// Auto-refresh JWKS if enabled
|
|
142
|
+
if (this.autoRefreshJWKS) {
|
|
143
|
+
this.startJWKSRefresh();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Fetch OIDC discovery document
|
|
149
|
+
*/
|
|
150
|
+
async fetchDiscovery() {
|
|
151
|
+
try {
|
|
152
|
+
const response = await fetch(this.discoveryUri);
|
|
153
|
+
|
|
154
|
+
if (!response.ok) {
|
|
155
|
+
throw new Error(`Failed to fetch discovery document: ${response.status}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
this.discoveryCache = await response.json();
|
|
159
|
+
|
|
160
|
+
// Update jwksUri from discovery if available
|
|
161
|
+
if (this.discoveryCache.jwks_uri) {
|
|
162
|
+
this.jwksUri = this.discoveryCache.jwks_uri;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return this.discoveryCache;
|
|
166
|
+
} catch (error) {
|
|
167
|
+
throw new Error(`Failed to fetch OIDC discovery: ${error.message}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Fetch JWKS from issuer
|
|
173
|
+
*/
|
|
174
|
+
async fetchJWKS(force = false) {
|
|
175
|
+
const now = Date.now();
|
|
176
|
+
|
|
177
|
+
// Return cached JWKS if still valid
|
|
178
|
+
if (!force && this.jwksCache && this.jwksCacheExpiry > now) {
|
|
179
|
+
return this.jwksCache;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
const response = await fetch(this.jwksUri);
|
|
184
|
+
|
|
185
|
+
if (!response.ok) {
|
|
186
|
+
throw new Error(`Failed to fetch JWKS: ${response.status}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const jwks = await response.json();
|
|
190
|
+
|
|
191
|
+
// Convert JWKs to PEM format and cache
|
|
192
|
+
for (const jwk of jwks.keys) {
|
|
193
|
+
if (jwk.kty === 'RSA' && jwk.use === 'sig') {
|
|
194
|
+
const publicKey = this.jwkToPem(jwk);
|
|
195
|
+
this.keys.set(jwk.kid, publicKey);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
this.jwksCache = jwks;
|
|
200
|
+
this.jwksCacheExpiry = now + this.jwksCacheTTL;
|
|
201
|
+
|
|
202
|
+
return jwks;
|
|
203
|
+
} catch (error) {
|
|
204
|
+
throw new Error(`Failed to fetch JWKS: ${error.message}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Convert JWK to PEM format
|
|
210
|
+
*/
|
|
211
|
+
jwkToPem(jwk) {
|
|
212
|
+
try {
|
|
213
|
+
// Use Node.js crypto to import JWK
|
|
214
|
+
const keyObject = createPublicKey({
|
|
215
|
+
key: jwk,
|
|
216
|
+
format: 'jwk'
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// Export as PEM
|
|
220
|
+
return keyObject.export({
|
|
221
|
+
type: 'spki',
|
|
222
|
+
format: 'pem'
|
|
223
|
+
});
|
|
224
|
+
} catch (error) {
|
|
225
|
+
throw new Error(`Failed to convert JWK to PEM: ${error.message}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Get public key by kid
|
|
231
|
+
*/
|
|
232
|
+
async getPublicKey(kid) {
|
|
233
|
+
let publicKey = this.keys.get(kid);
|
|
234
|
+
|
|
235
|
+
// If key not found, try refreshing JWKS
|
|
236
|
+
if (!publicKey) {
|
|
237
|
+
await this.fetchJWKS(true);
|
|
238
|
+
publicKey = this.keys.get(kid);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return publicKey;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Verify RS256 JWT token
|
|
246
|
+
*/
|
|
247
|
+
async verifyToken(token) {
|
|
248
|
+
try {
|
|
249
|
+
const parts = token.split('.');
|
|
250
|
+
if (parts.length !== 3) {
|
|
251
|
+
return { valid: false, error: 'Invalid token format' };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const [encodedHeader, encodedPayload, signature] = parts;
|
|
255
|
+
|
|
256
|
+
// Decode header
|
|
257
|
+
const header = JSON.parse(Buffer.from(encodedHeader, 'base64url').toString());
|
|
258
|
+
|
|
259
|
+
// Verify algorithm
|
|
260
|
+
if (header.alg !== 'RS256') {
|
|
261
|
+
return { valid: false, error: `Unsupported algorithm: ${header.alg}` };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Get public key
|
|
265
|
+
const publicKey = await this.getPublicKey(header.kid);
|
|
266
|
+
|
|
267
|
+
if (!publicKey) {
|
|
268
|
+
return { valid: false, error: `Public key not found for kid: ${header.kid}` };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Verify signature
|
|
272
|
+
const verify = createVerify('RSA-SHA256');
|
|
273
|
+
verify.update(`${encodedHeader}.${encodedPayload}`);
|
|
274
|
+
verify.end();
|
|
275
|
+
|
|
276
|
+
const isValid = verify.verify(publicKey, signature, 'base64url');
|
|
277
|
+
|
|
278
|
+
if (!isValid) {
|
|
279
|
+
return { valid: false, error: 'Invalid signature' };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Decode payload
|
|
283
|
+
const payload = JSON.parse(Buffer.from(encodedPayload, 'base64url').toString());
|
|
284
|
+
|
|
285
|
+
// Validate claims
|
|
286
|
+
const claimValidation = validateClaims(payload, {
|
|
287
|
+
issuer: this.issuer,
|
|
288
|
+
audience: this.audience,
|
|
289
|
+
clockTolerance: this.clockTolerance
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
if (!claimValidation.valid) {
|
|
293
|
+
return { valid: false, error: claimValidation.error };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
valid: true,
|
|
298
|
+
header,
|
|
299
|
+
payload
|
|
300
|
+
};
|
|
301
|
+
} catch (error) {
|
|
302
|
+
return { valid: false, error: error.message };
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Express middleware for OIDC authentication
|
|
308
|
+
*/
|
|
309
|
+
async middleware(req, res, next) {
|
|
310
|
+
try {
|
|
311
|
+
// Extract token from Authorization header
|
|
312
|
+
const authHeader = req.headers.authorization;
|
|
313
|
+
|
|
314
|
+
if (!authHeader) {
|
|
315
|
+
return res.status(401).json({
|
|
316
|
+
error: 'unauthorized',
|
|
317
|
+
error_description: 'Missing Authorization header'
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (!authHeader.startsWith('Bearer ')) {
|
|
322
|
+
return res.status(401).json({
|
|
323
|
+
error: 'unauthorized',
|
|
324
|
+
error_description: 'Invalid Authorization header format. Expected: Bearer <token>'
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const token = authHeader.substring(7);
|
|
329
|
+
|
|
330
|
+
if (!token) {
|
|
331
|
+
return res.status(401).json({
|
|
332
|
+
error: 'unauthorized',
|
|
333
|
+
error_description: 'Missing token'
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Verify token
|
|
338
|
+
const verification = await this.verifyToken(token);
|
|
339
|
+
|
|
340
|
+
if (!verification.valid) {
|
|
341
|
+
return res.status(401).json({
|
|
342
|
+
error: 'invalid_token',
|
|
343
|
+
error_description: verification.error
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Attach user to request
|
|
348
|
+
req.user = verification.payload;
|
|
349
|
+
req.token = token;
|
|
350
|
+
|
|
351
|
+
// Continue to next middleware
|
|
352
|
+
next();
|
|
353
|
+
} catch (error) {
|
|
354
|
+
res.status(500).json({
|
|
355
|
+
error: 'server_error',
|
|
356
|
+
error_description: error.message
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Start auto-refresh of JWKS
|
|
363
|
+
*/
|
|
364
|
+
startJWKSRefresh() {
|
|
365
|
+
// Refresh JWKS periodically (half of TTL to ensure fresh keys)
|
|
366
|
+
const refreshInterval = Math.floor(this.jwksCacheTTL / 2);
|
|
367
|
+
|
|
368
|
+
this.refreshInterval = setInterval(async () => {
|
|
369
|
+
try {
|
|
370
|
+
await this.fetchJWKS(true);
|
|
371
|
+
} catch (error) {
|
|
372
|
+
console.error('Failed to refresh JWKS:', error);
|
|
373
|
+
}
|
|
374
|
+
}, refreshInterval);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Stop auto-refresh of JWKS
|
|
379
|
+
*/
|
|
380
|
+
stopJWKSRefresh() {
|
|
381
|
+
if (this.refreshInterval) {
|
|
382
|
+
clearInterval(this.refreshInterval);
|
|
383
|
+
this.refreshInterval = null;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Introspect token via Authorization Server (RFC 7662)
|
|
389
|
+
*/
|
|
390
|
+
async introspectToken(token, clientId, clientSecret) {
|
|
391
|
+
if (!this.discoveryCache || !this.discoveryCache.introspection_endpoint) {
|
|
392
|
+
throw new Error('Introspection endpoint not available');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
try {
|
|
396
|
+
const response = await fetch(this.discoveryCache.introspection_endpoint, {
|
|
397
|
+
method: 'POST',
|
|
398
|
+
headers: {
|
|
399
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
400
|
+
'Authorization': `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`
|
|
401
|
+
},
|
|
402
|
+
body: new URLSearchParams({ token })
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
if (!response.ok) {
|
|
406
|
+
throw new Error(`Introspection failed: ${response.status}`);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return await response.json();
|
|
410
|
+
} catch (error) {
|
|
411
|
+
throw new Error(`Token introspection failed: ${error.message}`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Get discovery document
|
|
417
|
+
*/
|
|
418
|
+
getDiscovery() {
|
|
419
|
+
return this.discoveryCache;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Get cached JWKS
|
|
424
|
+
*/
|
|
425
|
+
getJWKS() {
|
|
426
|
+
return this.jwksCache;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Cleanup resources
|
|
431
|
+
*/
|
|
432
|
+
destroy() {
|
|
433
|
+
this.stopJWKSRefresh();
|
|
434
|
+
this.keys.clear();
|
|
435
|
+
this.jwksCache = null;
|
|
436
|
+
this.discoveryCache = null;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Create OIDC middleware factory for easy integration
|
|
442
|
+
*/
|
|
443
|
+
export function createOIDCMiddleware(options) {
|
|
444
|
+
const client = new OIDCClient(options);
|
|
445
|
+
|
|
446
|
+
// Return async middleware that initializes on first use
|
|
447
|
+
let initialized = false;
|
|
448
|
+
|
|
449
|
+
return async (req, res, next) => {
|
|
450
|
+
if (!initialized) {
|
|
451
|
+
await client.initialize();
|
|
452
|
+
initialized = true;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return client.middleware(req, res, next);
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export default {
|
|
460
|
+
OIDCClient,
|
|
461
|
+
createOIDCMiddleware
|
|
462
|
+
};
|