@sap/xssec
Version:
XS Advanced Container Security API for node.js
115 lines (97 loc) • 3.82 kB
JavaScript
const crypto = require("crypto");
const UnsupportedAlgorithmError = require('../error/validation/UnsupportedAlgorithmError');
const InvalidTokenSignatureError = require('../error/validation/InvalidTokenSignatureError');
/**
* @typedef {import("../token/Token")} Token
*/
const availableHashes = crypto.getHashes();
class Jwk {
key; // key information from JWKS response
pubKey; // Node.Js crypto public key
nodeAlg; // alg as Node.Js crypto name, e.g. RSA-SHA256
/**
* Creates a JWK based on a jwk-formatted public key
* @param {Object} key key information in jwk format
*/
constructor(key) {
this.key = key;
this.pubKey = crypto.createPublicKey({
key: key,
format: "jwk"
});
}
/**
* Creates a JWK based on a PEM-formatted public key
* @param {String} pem public key in PEM format
* @returns Jwk
*/
static fromPEM(pem) {
const cleanedUpPem = Jwk.cleanUpPemKey(pem);
const pubKey = crypto.createPublicKey({
key: cleanedUpPem,
format: "pem"
});
const jwk = pubKey.export({ format: "jwk" });
return new Jwk(jwk);
}
/**
* Validates if the token was signed with the private key that belongs to this public key.
* If available, uses the signature cache to avoid validating the same signature again on subsequent requests.
* @param {Token} token
* @param {import("../util/Types").Cache} [signatureCache] an optional cache for signature validation results
* @returns void if the signature is valid
* @throws {InvalidTokenSignatureError} when signature is invalid
*/
validateSignature(token, signatureCache) {
let validationResult = signatureCache?.get(token.jwt);
if (validationResult == null) {
validationResult = this.validateSignatureWithoutCache(token);
signatureCache?.set(token.jwt, validationResult);
}
if (validationResult !== true) {
throw new InvalidTokenSignatureError(token);
}
}
/**
* Validates if the token was signed with the private key that belongs to this public key without using a cache.
* @param {Token} token
* @returns {boolean} true if the signature is valid, false otherwise
*/
validateSignatureWithoutCache(token) {
const nodeAlg = Jwk.mapAlgToNodeAlg(token.header.alg);
if (!availableHashes.includes(nodeAlg)) {
throw new UnsupportedAlgorithmError(token, token.header.alg);
}
const [header, payload, signature] = token.jwt.split(".");
const verifier = crypto.createVerify(nodeAlg);
verifier.update(`${header}.${payload}`);
return verifier.verify(this.pubKey, signature, 'base64');
}
static mapAlgToNodeAlg(alg) {
switch (alg?.toUpperCase()) {
case "RS256":
return "RSA-SHA256";
case "RS384":
return "RSA-SHA384";
case "RS512":
return "RSA-SHA512";
default:
return null;
}
}
/*
* Adds missing line breaks to malformed PEM keys.
* For backward-compatibility, a specific kind of malformed PEM needs to be supported that is lacking line breaks around the header and footer.
* This kind of PEM input can occur, for example, in old service bindings of XSA and is not always fixable by consumers of this library.
*/
static cleanUpPemKey(pem = "") {
if (!pem.includes("KEY-----\n")) {
pem = pem.replace("KEY-----", "KEY-----\n");
}
if (!pem.includes("\n-----END")) {
pem = pem.replace("-----END", "\n-----END");
}
return pem;
}
}
module.exports = Jwk;