agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
206 lines • 9.14 kB
JavaScript
;
/**
* verify/jwtCore — the one place a signed token is checked, shared by every
* inbound verifier (`jwksIdentity`, `oidcIdentity`).
*
* Pattern: shared kernel. Two verifiers that each spelled "signature, issuer,
* audience, expiry" would be two answers to one question, and the day
* one of them required `exp` and the other did not is the day a token
* with no lifetime got through the lenient one.
* Role: load `jose`, map its failures onto the library's failure words, and
* hand back the verified claim set. Nothing here reads WHO the token
* is about: that is each verifier's own business.
*
* ── Every token expires ─────────────────────────────────────────────────────
* `jose` checks `exp` only when a token carries one (6.2.8,
* `validateClaimsSet`). A signed token WITHOUT `exp` would verify and never
* expire — another token type signed by the same keys, or a proxy that forgot
* the claim. So `exp` is required twice: `requiredClaims: ['exp']` for `jose`,
* and a check of the returned claims for any other backend (a stub, a fork)
* that ignores that option. Refused as `unverifiable`: the token is not one
* this door reads, and `expired` would tell the client to refresh.
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.loadJose = exports.verifySignedToken = exports.joseFailureOf = exports.DEFAULT_ALGORITHMS = exports.MissingJwksSupportError = void 0;
const errors_js_1 = require("../../../hosting/errors.js");
const lazyRequire_js_1 = require("../../../lib/lazyRequire.js");
/** Raised when a token must be verified and `jose` is not installed. */
class MissingJwksSupportError extends Error {
code = 'ERR_MISSING_JWKS_SUPPORT';
constructor() {
super('jwksIdentity requires the `jose` peer dependency.\n' +
' Install: npm install jose\n' +
' Or pass `backend` to jwksIdentity() if your bundler resolves it statically.');
this.name = 'MissingJwksSupportError';
}
}
exports.MissingJwksSupportError = MissingJwksSupportError;
/**
* The asymmetric families. Symmetric algorithms (`HS*`) are deliberately NOT
* here: with a JWKS the key is public, and a verifier that accepts an HMAC alg
* over a published key is the classic algorithm-confusion forgery.
*/
exports.DEFAULT_ALGORITHMS = [
'RS256',
'RS384',
'RS512',
'PS256',
'PS384',
'PS512',
'ES256',
'ES384',
'ES512',
];
/**
* `jose`'s stable error codes → this library's failure vocabulary.
*
* Mapped by CODE STRING rather than by `instanceof`, and that is the load-
* bearing choice: the error classes are not on `jose`'s main entry point (they
* live under `jose/errors`), the codes are documented as stable, and a string
* comparison survives a duplicated copy of the library in a `node_modules` tree
* — which `instanceof` famously does not.
*
* Every code is one the verifiers have SEEN the installed library produce; the
* pin test (test/adapters/identity/jwks.test.ts) reproduces each one.
*/
function joseFailureOf(err) {
const e = err;
const code = typeof e?.code === 'string' ? e.code : undefined;
switch (code) {
case 'ERR_JWT_EXPIRED':
return 'expired';
case 'ERR_JWT_CLAIM_VALIDATION_FAILED': {
const claim = typeof e?.claim === 'string' ? e.claim : undefined;
if (claim === 'aud')
return 'wrong-audience';
if (claim === 'iss')
return 'wrong-issuer';
if (claim === 'nbf')
return 'not-yet-valid';
// `exp` MISSING lands here too (claim 'exp', reason 'missing').
return 'unverifiable';
}
case 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED':
case 'ERR_JWKS_NO_MATCHING_KEY':
case 'ERR_JWKS_MULTIPLE_MATCHING_KEYS':
case 'ERR_JWS_INVALID':
case 'ERR_JWT_INVALID':
case 'ERR_JWK_INVALID':
case 'ERR_JWKS_INVALID':
case 'ERR_JOSE_ALG_NOT_ALLOWED':
case 'ERR_JOSE_NOT_SUPPORTED':
return 'unverifiable';
case 'ERR_JWKS_TIMEOUT':
return 'keys-unavailable';
default:
// No JOSE code at all. A key-set fetch that failed surfaces here as the
// runtime's own `TypeError: fetch failed` — an outage on THIS side, and
// answering it 401 would send every client to re-authenticate against a
// provider that is already down.
return code === undefined ? 'keys-unavailable' : 'unverifiable';
}
}
exports.joseFailureOf = joseFailureOf;
/**
* Check one token's signature and registered claims, and hand back its claim
* set — or throw {@link IdentityNotVerifiedError} (the caller's token) /
* {@link VerifierUnavailableError} (this deployment's key set).
*
* Neither error carries the token, the claims or the library's own text, and
* the original is deliberately NOT attached as `cause`: a cause travels into
* every serializer that walks own properties.
*/
async function verifySignedToken(jose, keys, token, checks) {
let payload;
try {
const verified = await jose.jwtVerify(token, keys, {
issuer: checks.issuer,
audience: checks.audience,
algorithms: checks.algorithms,
requiredClaims: ['exp'],
...(checks.clockToleranceSeconds !== undefined && {
clockTolerance: checks.clockToleranceSeconds,
}),
});
payload = verified.payload;
}
catch (err) {
const failure = joseFailureOf(err);
if (failure === 'keys-unavailable') {
throw new errors_js_1.VerifierUnavailableError(checks.verifierName, 'its key set could not be fetched');
}
throw new errors_js_1.IdentityNotVerifiedError(failure, false);
}
if (payload === null || typeof payload !== 'object') {
throw new errors_js_1.IdentityNotVerifiedError('unverifiable', false);
}
if (typeof payload.exp !== 'number' || !Number.isFinite(payload.exp)) {
// The second half of "every token expires": a backend that ignored
// `requiredClaims` does not get to hand back a token with no lifetime.
throw new errors_js_1.IdentityNotVerifiedError('unverifiable', false);
}
// And its TIME, not only its presence: a backend that skipped the clock
// checks (a stub, a fork) does not get to hand back an expired token either.
// The same tolerance `jose` was given, the same classes.
const nowSeconds = Math.floor(Date.now() / 1000);
const tolerance = checks.clockToleranceSeconds ?? 0;
// `<=`, as jose itself decides: a token is expired AT its `exp` second.
if (payload.exp + tolerance <= nowSeconds)
throw new errors_js_1.IdentityNotVerifiedError('expired', false);
if (payload.nbf !== undefined) {
if (typeof payload.nbf !== 'number' || !Number.isFinite(payload.nbf)) {
throw new errors_js_1.IdentityNotVerifiedError('unverifiable', false);
}
if (payload.nbf - tolerance > nowSeconds) {
throw new errors_js_1.IdentityNotVerifiedError('not-yet-valid', false);
}
}
return payload;
}
exports.verifySignedToken = verifySignedToken;
/**
* Load `jose`, or refuse by name.
*
* `jose` v6 is ESM-only, so a dynamic import is tried first and the CJS require
* is the fallback — one of the two works on every runtime this package
* supports. (The `unpdf` loader's shape, for the same reason.)
*/
async function loadJose() {
try {
const spec = 'jose';
return (await Promise.resolve(`${spec}`).then(s => __importStar(require(s))));
}
catch {
try {
return (0, lazyRequire_js_1.lazyRequire)('jose');
}
catch {
throw new MissingJwksSupportError();
}
}
}
exports.loadJose = loadJose;
//# sourceMappingURL=jwtCore.js.map