@oxy.so/core 1.3.1 → 1.5.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/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/mixins/OxyServices.agency.js +41 -0
- package/dist/cjs/mixins/OxyServices.utility.js +173 -6
- package/dist/cjs/server/auth.js +29 -0
- package/dist/cjs/server/index.js +17 -1
- package/dist/cjs/server/requesterAssertion.js +362 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/mixins/OxyServices.agency.js +41 -0
- package/dist/esm/mixins/OxyServices.utility.js +173 -6
- package/dist/esm/server/auth.js +28 -0
- package/dist/esm/server/index.js +4 -1
- package/dist/esm/server/requesterAssertion.js +353 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/mixins/OxyServices.agency.d.ts +41 -0
- package/dist/types/mixins/OxyServices.utility.d.ts +49 -0
- package/dist/types/server/auth.d.ts +19 -1
- package/dist/types/server/index.d.ts +4 -2
- package/dist/types/server/requesterAssertion.d.ts +151 -0
- package/package.json +1 -1
- package/src/index.ts +3 -1
- package/src/mixins/OxyServices.agency.ts +76 -0
- package/src/mixins/OxyServices.utility.ts +226 -5
- package/src/mixins/__tests__/authRefusalObservability.test.ts +396 -0
- package/src/server/__tests__/requesterAssertion.test.ts +359 -0
- package/src/server/auth.ts +37 -1
- package/src/server/index.ts +29 -0
- package/src/server/requesterAssertion.ts +465 -0
|
@@ -109,5 +109,46 @@ function OxyServicesAgencyMixin(Base) {
|
|
|
109
109
|
throw this.handleError(error);
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Product backend → Oxy: trade the signed-in requester's access token for a
|
|
114
|
+
* one-use assertion naming `agentId` (ADR 0025). Authenticated with THIS
|
|
115
|
+
* instance's service credential (`configureServiceAuth`). `subjectToken` is
|
|
116
|
+
* sent to Oxy only, in the body; never forward it anywhere else.
|
|
117
|
+
*
|
|
118
|
+
* Not retried: a refusal is an answer, and the caller mints per turn.
|
|
119
|
+
*/
|
|
120
|
+
async mintRequesterAssertion(input) {
|
|
121
|
+
try {
|
|
122
|
+
const serviceToken = await this.getServiceToken();
|
|
123
|
+
return await this.makeRequest('POST', '/internal/native-agents/requester-assertions', { agentId: input.agentId, subjectToken: input.subjectToken }, {
|
|
124
|
+
cache: false,
|
|
125
|
+
retry: false,
|
|
126
|
+
timeout: 5000,
|
|
127
|
+
headers: { Authorization: `Bearer ${serviceToken}` },
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
throw this.handleError(error);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Audience → Oxy: verify, live-revalidate and CONSUME an assertion. A second
|
|
136
|
+
* call with the same assertion answers `active: false`. Authenticated with
|
|
137
|
+
* the audience's own service credential.
|
|
138
|
+
*/
|
|
139
|
+
async introspectRequesterAssertion(input) {
|
|
140
|
+
try {
|
|
141
|
+
const serviceToken = await this.getServiceToken();
|
|
142
|
+
return await this.makeRequest('POST', '/internal/native-agents/requester-assertions/introspect', { assertion: input.assertion, presenter: input.presenter }, {
|
|
143
|
+
cache: false,
|
|
144
|
+
retry: false,
|
|
145
|
+
timeout: 5000,
|
|
146
|
+
headers: { Authorization: `Bearer ${serviceToken}` },
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
throw this.handleError(error);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
112
153
|
};
|
|
113
154
|
}
|
|
@@ -212,10 +212,51 @@ function OxyServicesUtilityMixin(Base) {
|
|
|
212
212
|
* @returns Express middleware function
|
|
213
213
|
*/
|
|
214
214
|
auth(options = {}) {
|
|
215
|
-
const { debug = false, onError, loadUser = false, optional = false, jwtSecret, serviceTokenJwksUrl = new URL('/.well-known/jwks.json', this.getBaseURL()).toString(), expectedIssuer = OXY_JWT_ISSUER, expectedAudience = OXY_JWT_AUDIENCE, } = options;
|
|
215
|
+
const { debug = false, onError, onRefusal, loadUser = false, optional = false, jwtSecret, serviceTokenJwksUrl = new URL('/.well-known/jwks.json', this.getBaseURL()).toString(), expectedIssuer = OXY_JWT_ISSUER, expectedAudience = OXY_JWT_AUDIENCE, } = options;
|
|
216
216
|
// Cross-mixin method access: typed as a structural subset of the
|
|
217
217
|
// composed OxyServices we know we have at runtime.
|
|
218
218
|
const oxyInstance = this;
|
|
219
|
+
/**
|
|
220
|
+
* Make a refusal observable to the HOST, without telling the client
|
|
221
|
+
* anything it is not already told.
|
|
222
|
+
*
|
|
223
|
+
* Called at the top of every branch that rejects a PRESENTED credential,
|
|
224
|
+
* including the ones `optional` then swallows — those are the expensive
|
|
225
|
+
* ones, because they leave no trace anywhere else. The response bodies
|
|
226
|
+
* below are untouched by design: what a client sees does not change.
|
|
227
|
+
*
|
|
228
|
+
* `warn`, not `debug`: a debug-gated reason is only there for whoever
|
|
229
|
+
* already suspects this middleware, and the failure mode this exists for
|
|
230
|
+
* is the one where nobody does. The volume is bounded by refusals, and a
|
|
231
|
+
* host drowning in them has a fault worth the lines.
|
|
232
|
+
*/
|
|
233
|
+
const recordRefusal = (req, refusal) => {
|
|
234
|
+
const recorded = { ...refusal, optional };
|
|
235
|
+
req.oxyAuthRefusal = recorded;
|
|
236
|
+
logger_1.logger.warn(`[oxy.auth] refused ${recorded.code}: ${recorded.reason}`, {
|
|
237
|
+
component: 'auth',
|
|
238
|
+
method: 'auth',
|
|
239
|
+
code: recorded.code,
|
|
240
|
+
stage: recorded.stage,
|
|
241
|
+
reason: recorded.reason,
|
|
242
|
+
status: recorded.status,
|
|
243
|
+
// Says whether the client will see this status or the host's own
|
|
244
|
+
// generic answer — the difference that made the JWKS outage invisible.
|
|
245
|
+
optional: recorded.optional,
|
|
246
|
+
path: req.path,
|
|
247
|
+
});
|
|
248
|
+
if (onRefusal) {
|
|
249
|
+
try {
|
|
250
|
+
onRefusal(recorded);
|
|
251
|
+
}
|
|
252
|
+
catch (observerError) {
|
|
253
|
+
logger_1.logger.warn('[oxy.auth] onRefusal observer threw', {
|
|
254
|
+
component: 'auth',
|
|
255
|
+
method: 'auth',
|
|
256
|
+
}, observerError);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
};
|
|
219
260
|
// Return an async middleware function
|
|
220
261
|
return async (req, res, next) => {
|
|
221
262
|
try {
|
|
@@ -253,6 +294,12 @@ function OxyServicesUtilityMixin(Base) {
|
|
|
253
294
|
decoded = (0, jwt_decode_1.jwtDecode)(token);
|
|
254
295
|
}
|
|
255
296
|
catch (decodeError) {
|
|
297
|
+
recordRefusal(req, {
|
|
298
|
+
code: 'INVALID_TOKEN_FORMAT',
|
|
299
|
+
stage: 'token',
|
|
300
|
+
reason: 'Bearer credential is not a decodable JWT',
|
|
301
|
+
status: 401,
|
|
302
|
+
});
|
|
256
303
|
if (debug) {
|
|
257
304
|
logger_1.logger.debug('[oxy.auth] Token decode failed', {
|
|
258
305
|
component: 'auth',
|
|
@@ -299,6 +346,12 @@ function OxyServicesUtilityMixin(Base) {
|
|
|
299
346
|
// else (e.g. Node crypto failing to load on a misconfigured host)
|
|
300
347
|
// genuinely IS a 500.
|
|
301
348
|
if (verifyError instanceof ServiceTokenConfigurationError) {
|
|
349
|
+
recordRefusal(req, {
|
|
350
|
+
code: 'SERVICE_TOKEN_NOT_CONFIGURED',
|
|
351
|
+
stage: 'service-token',
|
|
352
|
+
reason: verifyError.message,
|
|
353
|
+
status: 403,
|
|
354
|
+
});
|
|
302
355
|
if (optional) {
|
|
303
356
|
req.userId = null;
|
|
304
357
|
req.user = null;
|
|
@@ -317,6 +370,20 @@ function OxyServicesUtilityMixin(Base) {
|
|
|
317
370
|
if (verifyError instanceof ServiceTokenStructureError ||
|
|
318
371
|
verifyError instanceof ServiceTokenSignatureError ||
|
|
319
372
|
verifyError instanceof ServiceTokenClaimError) {
|
|
373
|
+
const code = verifyError instanceof ServiceTokenClaimError
|
|
374
|
+
? 'INVALID_SERVICE_TOKEN_CLAIMS'
|
|
375
|
+
: 'INVALID_SERVICE_TOKEN';
|
|
376
|
+
// The SDK's own message is the whole diagnostic value here —
|
|
377
|
+
// "signing key is unknown" (rotated/unpublished kid), "key set
|
|
378
|
+
// is unavailable" (JWKS empty, unreachable or malformed) and
|
|
379
|
+
// "audience mismatch" are three different outages that answer
|
|
380
|
+
// the client identically.
|
|
381
|
+
recordRefusal(req, {
|
|
382
|
+
code,
|
|
383
|
+
stage: 'service-token',
|
|
384
|
+
reason: `${verifyError.name}: ${verifyError.message}`,
|
|
385
|
+
status: 401,
|
|
386
|
+
});
|
|
320
387
|
if (debug) {
|
|
321
388
|
logger_1.logger.debug('[oxy.auth] Service token rejected', {
|
|
322
389
|
component: 'auth',
|
|
@@ -330,11 +397,6 @@ function OxyServicesUtilityMixin(Base) {
|
|
|
330
397
|
req.user = null;
|
|
331
398
|
return next();
|
|
332
399
|
}
|
|
333
|
-
const code = verifyError instanceof ServiceTokenSignatureError
|
|
334
|
-
? 'INVALID_SERVICE_TOKEN'
|
|
335
|
-
: verifyError instanceof ServiceTokenStructureError
|
|
336
|
-
? 'INVALID_SERVICE_TOKEN'
|
|
337
|
-
: 'INVALID_SERVICE_TOKEN_CLAIMS';
|
|
338
400
|
const error = {
|
|
339
401
|
error: code,
|
|
340
402
|
message: verifyError.message,
|
|
@@ -362,6 +424,14 @@ function OxyServicesUtilityMixin(Base) {
|
|
|
362
424
|
// Check expiration — reject tokens at exact expiry second (use <=)
|
|
363
425
|
const now = Math.floor(Date.now() / 1000);
|
|
364
426
|
if (!Number.isInteger(decoded.exp) || decoded.exp <= now) {
|
|
427
|
+
recordRefusal(req, {
|
|
428
|
+
code: 'TOKEN_EXPIRED',
|
|
429
|
+
stage: 'service-token',
|
|
430
|
+
reason: Number.isInteger(decoded.exp)
|
|
431
|
+
? 'Service token expired'
|
|
432
|
+
: 'Service token has no integer exp claim',
|
|
433
|
+
status: 401,
|
|
434
|
+
});
|
|
365
435
|
if (optional) {
|
|
366
436
|
req.userId = null;
|
|
367
437
|
req.user = null;
|
|
@@ -373,6 +443,12 @@ function OxyServicesUtilityMixin(Base) {
|
|
|
373
443
|
return res.status(401).json(error);
|
|
374
444
|
}
|
|
375
445
|
if (decoded.nbf !== undefined && (!Number.isInteger(decoded.nbf) || decoded.nbf > now)) {
|
|
446
|
+
recordRefusal(req, {
|
|
447
|
+
code: 'INVALID_SERVICE_TOKEN_CLAIMS',
|
|
448
|
+
stage: 'service-token',
|
|
449
|
+
reason: 'Service token is not yet valid (nbf)',
|
|
450
|
+
status: 401,
|
|
451
|
+
});
|
|
376
452
|
const error = { error: 'INVALID_SERVICE_TOKEN_CLAIMS', message: 'Service token is not yet valid', code: 'INVALID_SERVICE_TOKEN_CLAIMS', status: 401 };
|
|
377
453
|
if (onError)
|
|
378
454
|
return onError(error);
|
|
@@ -394,6 +470,22 @@ function OxyServicesUtilityMixin(Base) {
|
|
|
394
470
|
|| !Array.isArray(decoded.scopes)
|
|
395
471
|
|| !decoded.scopes.every((scope) => typeof scope === 'string' && scope.length > 0 && scope === scope.trim())
|
|
396
472
|
|| new Set(decoded.scopes).size !== decoded.scopes.length) {
|
|
473
|
+
recordRefusal(req, {
|
|
474
|
+
code: 'INVALID_SERVICE_TOKEN',
|
|
475
|
+
stage: 'service-token',
|
|
476
|
+
// Names the field rather than its value: an exact-match claim
|
|
477
|
+
// failing on invisible whitespace reads as "present" in a log
|
|
478
|
+
// that only says which claims were missing.
|
|
479
|
+
reason: `Service token claims unusable (${[
|
|
480
|
+
!isExactNonEmptyServiceClaim(appId) ? 'appId' : null,
|
|
481
|
+
!isExactNonEmptyServiceClaim(decoded.appName) ? 'appName' : null,
|
|
482
|
+
!isExactNonEmptyServiceClaim(credentialId) ? 'credentialId' : null,
|
|
483
|
+
!isExactNonEmptyServiceClaim(ownerAccountId) ? 'ownerAccountId' : null,
|
|
484
|
+
!isOxyServiceEnvironment(environment) ? 'environment' : null,
|
|
485
|
+
!Array.isArray(decoded.scopes) ? 'scopes' : null,
|
|
486
|
+
].filter((field) => field !== null).join(', ') || 'scopes'})`,
|
|
487
|
+
status: 401,
|
|
488
|
+
});
|
|
397
489
|
if (optional) {
|
|
398
490
|
req.userId = null;
|
|
399
491
|
req.user = null;
|
|
@@ -408,6 +500,12 @@ function OxyServicesUtilityMixin(Base) {
|
|
|
408
500
|
const oxyUserIdRaw = req.headers['x-oxy-user-id'];
|
|
409
501
|
const oxyUserId = isExactNonEmptyServiceClaim(oxyUserIdRaw) ? oxyUserIdRaw : null;
|
|
410
502
|
if (oxyUserIdRaw !== undefined && oxyUserId === null) {
|
|
503
|
+
recordRefusal(req, {
|
|
504
|
+
code: 'INVALID_SERVICE_TOKEN_CLAIMS',
|
|
505
|
+
stage: 'service-token',
|
|
506
|
+
reason: 'X-Oxy-User-Id is not an exact non-empty id',
|
|
507
|
+
status: 401,
|
|
508
|
+
});
|
|
411
509
|
const error = {
|
|
412
510
|
error: 'INVALID_SERVICE_TOKEN_CLAIMS',
|
|
413
511
|
message: 'Delegated user id must be an exact non-empty id',
|
|
@@ -432,6 +530,16 @@ function OxyServicesUtilityMixin(Base) {
|
|
|
432
530
|
appId,
|
|
433
531
|
attemptedUserId: oxyUserId,
|
|
434
532
|
});
|
|
533
|
+
// A verifier with no service credentials of its own cannot
|
|
534
|
+
// reach the verify endpoint at all, so `null` here can mean
|
|
535
|
+
// "no grant" OR "this host cannot ask". Both refuse, and both
|
|
536
|
+
// are worth naming on the request.
|
|
537
|
+
recordRefusal(req, {
|
|
538
|
+
code: 'SERVICE_ACTING_AS_UNAUTHORIZED',
|
|
539
|
+
stage: 'service-token',
|
|
540
|
+
reason: `No delegation grant for app ${appId} acting as ${oxyUserId} (or this verifier could not reach the grant check)`,
|
|
541
|
+
status: 403,
|
|
542
|
+
});
|
|
435
543
|
const error = {
|
|
436
544
|
error: 'SERVICE_ACTING_AS_UNAUTHORIZED',
|
|
437
545
|
message: 'Service not authorized to act as this user',
|
|
@@ -477,6 +585,12 @@ function OxyServicesUtilityMixin(Base) {
|
|
|
477
585
|
// compared against the id the validated session resolves to.
|
|
478
586
|
const claimedUserId = readStringClaim(decoded.userId) ?? readStringClaim(decoded.id);
|
|
479
587
|
if (!claimedUserId) {
|
|
588
|
+
recordRefusal(req, {
|
|
589
|
+
code: 'INVALID_TOKEN_PAYLOAD',
|
|
590
|
+
stage: 'token',
|
|
591
|
+
reason: 'Token carries no usable user id claim',
|
|
592
|
+
status: 401,
|
|
593
|
+
});
|
|
480
594
|
if (optional) {
|
|
481
595
|
req.userId = null;
|
|
482
596
|
req.user = null;
|
|
@@ -495,6 +609,12 @@ function OxyServicesUtilityMixin(Base) {
|
|
|
495
609
|
// Check token expiration locally first (fast path)
|
|
496
610
|
// Reject tokens at exact expiry second (use <=)
|
|
497
611
|
if (decoded.exp && decoded.exp <= Math.floor(Date.now() / 1000)) {
|
|
612
|
+
recordRefusal(req, {
|
|
613
|
+
code: 'TOKEN_EXPIRED',
|
|
614
|
+
stage: 'token',
|
|
615
|
+
reason: 'User access token expired',
|
|
616
|
+
status: 401,
|
|
617
|
+
});
|
|
498
618
|
if (optional) {
|
|
499
619
|
req.userId = null;
|
|
500
620
|
req.user = null;
|
|
@@ -517,6 +637,12 @@ function OxyServicesUtilityMixin(Base) {
|
|
|
517
637
|
// required this.
|
|
518
638
|
const sessionId = readStringClaim(decoded.sessionId);
|
|
519
639
|
if (!sessionId) {
|
|
640
|
+
recordRefusal(req, {
|
|
641
|
+
code: 'SESSION_REQUIRED',
|
|
642
|
+
stage: 'token',
|
|
643
|
+
reason: 'User access token is not bound to a session',
|
|
644
|
+
status: 401,
|
|
645
|
+
});
|
|
520
646
|
if (optional) {
|
|
521
647
|
req.userId = null;
|
|
522
648
|
req.user = null;
|
|
@@ -539,6 +665,12 @@ function OxyServicesUtilityMixin(Base) {
|
|
|
539
665
|
useHeaderValidation: true,
|
|
540
666
|
});
|
|
541
667
|
if (!validationResult || !validationResult.valid || !validationResult.user) {
|
|
668
|
+
recordRefusal(req, {
|
|
669
|
+
code: 'INVALID_SESSION',
|
|
670
|
+
stage: 'session',
|
|
671
|
+
reason: 'Session is invalid, revoked or expired',
|
|
672
|
+
status: 401,
|
|
673
|
+
});
|
|
542
674
|
if (optional) {
|
|
543
675
|
req.userId = null;
|
|
544
676
|
req.user = null;
|
|
@@ -557,6 +689,12 @@ function OxyServicesUtilityMixin(Base) {
|
|
|
557
689
|
// The session — not the token — is the source of truth for identity.
|
|
558
690
|
const validatedUserId = getUserIdentityId(validationResult.user);
|
|
559
691
|
if (!validatedUserId) {
|
|
692
|
+
recordRefusal(req, {
|
|
693
|
+
code: 'INVALID_SESSION',
|
|
694
|
+
stage: 'session',
|
|
695
|
+
reason: 'Session did not resolve to a usable identity',
|
|
696
|
+
status: 401,
|
|
697
|
+
});
|
|
560
698
|
if (optional) {
|
|
561
699
|
req.userId = null;
|
|
562
700
|
req.user = null;
|
|
@@ -582,6 +720,12 @@ function OxyServicesUtilityMixin(Base) {
|
|
|
582
720
|
claimedUserId,
|
|
583
721
|
validatedUserId,
|
|
584
722
|
});
|
|
723
|
+
recordRefusal(req, {
|
|
724
|
+
code: 'SESSION_USER_MISMATCH',
|
|
725
|
+
stage: 'session',
|
|
726
|
+
reason: 'Token user claim does not own the presented session',
|
|
727
|
+
status: 401,
|
|
728
|
+
});
|
|
585
729
|
if (optional) {
|
|
586
730
|
req.userId = null;
|
|
587
731
|
req.user = null;
|
|
@@ -612,6 +756,15 @@ function OxyServicesUtilityMixin(Base) {
|
|
|
612
756
|
return next();
|
|
613
757
|
}
|
|
614
758
|
catch (validationError) {
|
|
759
|
+
recordRefusal(req, {
|
|
760
|
+
code: 'SESSION_VALIDATION_ERROR',
|
|
761
|
+
stage: 'session',
|
|
762
|
+
// Name and transport code only. The message of a failed
|
|
763
|
+
// `validateSession` can carry the request URL, and that URL
|
|
764
|
+
// contains the session id — which is a credential.
|
|
765
|
+
reason: `Session validation call failed (${describeErrorSafely(validationError)})`,
|
|
766
|
+
status: 401,
|
|
767
|
+
});
|
|
615
768
|
if (debug) {
|
|
616
769
|
logger_1.logger.debug('[oxy.auth] Session validation failed', {
|
|
617
770
|
component: 'auth',
|
|
@@ -1037,6 +1190,20 @@ async function verifyServiceTokenSignature(token, options) {
|
|
|
1037
1190
|
* those values out of URL construction and identity comparison, so an
|
|
1038
1191
|
* unexpected shape becomes a 401 rather than a stringified surprise.
|
|
1039
1192
|
*/
|
|
1193
|
+
/**
|
|
1194
|
+
* A short, log-safe description of a thrown value: its class name and, when the
|
|
1195
|
+
* transport attached one, its `code` (`ECONNREFUSED`, `ETIMEDOUT`, `ENOTFOUND`
|
|
1196
|
+
* …). Never the message — a failed request's message can quote the URL it was
|
|
1197
|
+
* made against, and a session-validation URL contains the session id.
|
|
1198
|
+
*/
|
|
1199
|
+
function describeErrorSafely(error) {
|
|
1200
|
+
if (!(error instanceof Error))
|
|
1201
|
+
return typeof error;
|
|
1202
|
+
const code = error.code;
|
|
1203
|
+
return typeof code === 'string' && /^[A-Za-z0-9_]{1,40}$/.test(code)
|
|
1204
|
+
? `${error.name}: ${code}`
|
|
1205
|
+
: error.name;
|
|
1206
|
+
}
|
|
1040
1207
|
function readStringClaim(value) {
|
|
1041
1208
|
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
1042
1209
|
}
|
package/dist/cjs/server/auth.js
CHANGED
|
@@ -8,9 +8,11 @@ exports.getRequiredOxyBillingPrincipal = getRequiredOxyBillingPrincipal;
|
|
|
8
8
|
exports.getOxyDelegatedUserId = getOxyDelegatedUserId;
|
|
9
9
|
exports.getOxyRequestAttribution = getOxyRequestAttribution;
|
|
10
10
|
exports.getRequiredOxyUserId = getRequiredOxyUserId;
|
|
11
|
+
exports.getOxyAuthRefusal = getOxyAuthRefusal;
|
|
11
12
|
exports.requireOxyAuth = requireOxyAuth;
|
|
12
13
|
exports.createOptionalOxyAuth = createOptionalOxyAuth;
|
|
13
14
|
exports.createOxyAuthMiddleware = createOxyAuthMiddleware;
|
|
15
|
+
const logger_1 = require("../logger");
|
|
14
16
|
const oxyServiceEnvironment_1 = require("../utils/oxyServiceEnvironment");
|
|
15
17
|
Object.defineProperty(exports, "OXY_SERVICE_ENVIRONMENTS", { enumerable: true, get: function () { return oxyServiceEnvironment_1.OXY_SERVICE_ENVIRONMENTS; } });
|
|
16
18
|
function normalizeId(value) {
|
|
@@ -119,9 +121,36 @@ function getRequiredOxyUserId(req) {
|
|
|
119
121
|
}
|
|
120
122
|
return userId;
|
|
121
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* Why the credential on this request was refused, or `null` when none was
|
|
126
|
+
* presented (or it was accepted).
|
|
127
|
+
*
|
|
128
|
+
* Set by `oxy.auth()` — including the `optional: true` mount the composed
|
|
129
|
+
* helpers use, where a refusal otherwise leaves no trace: the request simply
|
|
130
|
+
* arrives unauthenticated and the host answers its own generic 401. Hosts log
|
|
131
|
+
* this beside their own 401; it is diagnostic and **must not be put into a
|
|
132
|
+
* response body**, which is exactly what it is kept out of.
|
|
133
|
+
*/
|
|
134
|
+
function getOxyAuthRefusal(req) {
|
|
135
|
+
return req.oxyAuthRefusal ?? null;
|
|
136
|
+
}
|
|
122
137
|
function requireOxyAuth(req, res, next) {
|
|
123
138
|
const userId = getOxyUserId(req);
|
|
124
139
|
if (!userId) {
|
|
140
|
+
// The body stays exactly as generic as it was. The LOG does not: this 401
|
|
141
|
+
// is the one a caller sees when the optional resolver already refused a
|
|
142
|
+
// real credential, and printing that code here is what turns "Homiio got
|
|
143
|
+
// 401, Alia logged nothing" into one greppable line.
|
|
144
|
+
const refusal = getOxyAuthRefusal(req);
|
|
145
|
+
if (refusal) {
|
|
146
|
+
logger_1.logger.warn(`[oxy.auth] 401 after refusal ${refusal.code}: ${refusal.reason}`, {
|
|
147
|
+
component: 'auth',
|
|
148
|
+
method: 'requireOxyAuth',
|
|
149
|
+
code: refusal.code,
|
|
150
|
+
stage: refusal.stage,
|
|
151
|
+
reason: refusal.reason,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
125
154
|
res.status(401).json({
|
|
126
155
|
error: 'Unauthorized',
|
|
127
156
|
message: 'Authentication required',
|
package/dist/cjs/server/index.js
CHANGED
|
@@ -16,10 +16,12 @@
|
|
|
16
16
|
* ```
|
|
17
17
|
*/
|
|
18
18
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
-
exports.
|
|
19
|
+
exports.signOxyRequesterAssertion = exports.readOxyRequesterAssertionKeyId = exports.oxyRequesterAssertionClaimsSchema = exports.OxyRequesterAssertionError = exports.OXY_REQUESTER_ASSERTION_TYPE = exports.OXY_REQUESTER_ASSERTION_MAX_TTL_SECONDS = exports.OXY_REQUESTER_ASSERTION_HEADER = exports.OXY_REQUESTER_ASSERTION_DEFAULT_ISSUER = exports.createOxyRequesterAssertionAuth = exports.createOxyJwksKeyResolver = exports.verifyCapabilityTicket = exports.readCapabilityAuthorization = exports.issueCapabilityTicket = exports.inputSatisfiesCapabilityLimits = exports.createCapabilityTicketMiddleware = exports.CapabilityTicketError = exports.OXY_CSP_BASELINE = exports.inlineScriptCspHash = exports.formatOxyCspPolicy = exports.extractInlineScripts = exports.cspSourcesFor = exports.createOxySecurityHeaders = exports.buildOxyPagesHeaders = exports.buildOxyCspDirectives = exports.createOxyCors = exports.UPSTREAM_HEADERS_TIMEOUT_MS = exports.MAX_URL_LENGTH = exports.MAX_REDIRECTS = exports.DEFAULT_USER_AGENT = exports.BLOCKED_HOSTNAMES = exports.ALLOWED_PROTOCOLS = exports.ALLOWED_PORTS = exports.UpstreamError = exports.SsrfRejection = exports.safeFetch = exports.isBlockedIp = exports.assertSafePublicUrl = exports.createOxyRateLimit = exports.OXY_SERVICE_ENVIRONMENTS = exports.requireOxyAuth = exports.isOxyAuthenticated = exports.getRequiredOxyUserId = exports.getRequiredOxyBillingPrincipal = exports.getOxyUserId = exports.getOxyRequestAttribution = exports.getOxyDelegatedUserId = exports.getOxyBillingPrincipal = exports.getOxyAuthRefusal = exports.createOxyAuthMiddleware = exports.createOptionalOxyAuth = void 0;
|
|
20
|
+
exports.withoutNodeHttpObservation = exports.observeNodeHttp = exports.createEcosystemTraffic = exports.isOfficialWebOrigin = exports.registrableApex = exports.OXY_IDENTITY_CACHE_PREFIXES = exports.oxyUserByIdCacheKey = exports.evictOxyIdentityCache = exports.publishOxyUserInvalidation = exports.createOxyUserInvalidationHandler = exports.verifySecret = exports.verifyOxyRequesterAssertion = void 0;
|
|
20
21
|
var auth_1 = require("./auth");
|
|
21
22
|
Object.defineProperty(exports, "createOptionalOxyAuth", { enumerable: true, get: function () { return auth_1.createOptionalOxyAuth; } });
|
|
22
23
|
Object.defineProperty(exports, "createOxyAuthMiddleware", { enumerable: true, get: function () { return auth_1.createOxyAuthMiddleware; } });
|
|
24
|
+
Object.defineProperty(exports, "getOxyAuthRefusal", { enumerable: true, get: function () { return auth_1.getOxyAuthRefusal; } });
|
|
23
25
|
Object.defineProperty(exports, "getOxyBillingPrincipal", { enumerable: true, get: function () { return auth_1.getOxyBillingPrincipal; } });
|
|
24
26
|
Object.defineProperty(exports, "getOxyDelegatedUserId", { enumerable: true, get: function () { return auth_1.getOxyDelegatedUserId; } });
|
|
25
27
|
Object.defineProperty(exports, "getOxyRequestAttribution", { enumerable: true, get: function () { return auth_1.getOxyRequestAttribution; } });
|
|
@@ -72,6 +74,20 @@ Object.defineProperty(exports, "inputSatisfiesCapabilityLimits", { enumerable: t
|
|
|
72
74
|
Object.defineProperty(exports, "issueCapabilityTicket", { enumerable: true, get: function () { return capabilityTicket_1.issueCapabilityTicket; } });
|
|
73
75
|
Object.defineProperty(exports, "readCapabilityAuthorization", { enumerable: true, get: function () { return capabilityTicket_1.readCapabilityAuthorization; } });
|
|
74
76
|
Object.defineProperty(exports, "verifyCapabilityTicket", { enumerable: true, get: function () { return capabilityTicket_1.verifyCapabilityTicket; } });
|
|
77
|
+
// Present-requester assertions (ADR 0025): a product backend trades a signed-in
|
|
78
|
+
// person's live session with Oxy for a one-use assertion its audience consumes.
|
|
79
|
+
var requesterAssertion_1 = require("./requesterAssertion");
|
|
80
|
+
Object.defineProperty(exports, "createOxyJwksKeyResolver", { enumerable: true, get: function () { return requesterAssertion_1.createOxyJwksKeyResolver; } });
|
|
81
|
+
Object.defineProperty(exports, "createOxyRequesterAssertionAuth", { enumerable: true, get: function () { return requesterAssertion_1.createOxyRequesterAssertionAuth; } });
|
|
82
|
+
Object.defineProperty(exports, "OXY_REQUESTER_ASSERTION_DEFAULT_ISSUER", { enumerable: true, get: function () { return requesterAssertion_1.OXY_REQUESTER_ASSERTION_DEFAULT_ISSUER; } });
|
|
83
|
+
Object.defineProperty(exports, "OXY_REQUESTER_ASSERTION_HEADER", { enumerable: true, get: function () { return requesterAssertion_1.OXY_REQUESTER_ASSERTION_HEADER; } });
|
|
84
|
+
Object.defineProperty(exports, "OXY_REQUESTER_ASSERTION_MAX_TTL_SECONDS", { enumerable: true, get: function () { return requesterAssertion_1.OXY_REQUESTER_ASSERTION_MAX_TTL_SECONDS; } });
|
|
85
|
+
Object.defineProperty(exports, "OXY_REQUESTER_ASSERTION_TYPE", { enumerable: true, get: function () { return requesterAssertion_1.OXY_REQUESTER_ASSERTION_TYPE; } });
|
|
86
|
+
Object.defineProperty(exports, "OxyRequesterAssertionError", { enumerable: true, get: function () { return requesterAssertion_1.OxyRequesterAssertionError; } });
|
|
87
|
+
Object.defineProperty(exports, "oxyRequesterAssertionClaimsSchema", { enumerable: true, get: function () { return requesterAssertion_1.oxyRequesterAssertionClaimsSchema; } });
|
|
88
|
+
Object.defineProperty(exports, "readOxyRequesterAssertionKeyId", { enumerable: true, get: function () { return requesterAssertion_1.readOxyRequesterAssertionKeyId; } });
|
|
89
|
+
Object.defineProperty(exports, "signOxyRequesterAssertion", { enumerable: true, get: function () { return requesterAssertion_1.signOxyRequesterAssertion; } });
|
|
90
|
+
Object.defineProperty(exports, "verifyOxyRequesterAssertion", { enumerable: true, get: function () { return requesterAssertion_1.verifyOxyRequesterAssertion; } });
|
|
75
91
|
// Constant-time secret comparison.
|
|
76
92
|
var verifySecret_1 = require("./verifySecret");
|
|
77
93
|
Object.defineProperty(exports, "verifySecret", { enumerable: true, get: function () { return verifySecret_1.verifySecret; } });
|