UNPKG

@nuvo-prime/np-samlify

Version:

High-level API for Single Sign On (SAML 2.0)

737 lines (668 loc) 31.5 kB
/** * @file SamlLib.js * @author tngan * @desc A simple library including some common functions */ import utility, { flattenDeep, isString } from './utility'; import { algorithms, wording, namespace } from './urn'; import { select } from 'xpath'; import { MetadataInterface } from './metadata'; import nrsa, { SigningSchemeHash } from 'node-rsa'; import { SignedXml, FileKeyInfo } from 'xml-crypto'; import * as xmlenc from '@nuvo-prime/xml-encryption'; import { extract } from './extractor'; import camelCase from 'camelcase'; import { getContext } from './api'; import xmlEscape from 'xml-escape'; const signatureAlgorithms = algorithms.signature; const digestAlgorithms = algorithms.digest; const certUse = wording.certUse; const urlParams = wording.urlParams; export interface SignatureConstructor { rawSamlMessage: string; referenceTagXPath?: string; privateKey: string; privateKeyPass?: string; signatureAlgorithm: string; signingCert: string | Buffer; isBase64Output?: boolean; signatureConfig?: any; isMessageSigned?: boolean; transformationAlgorithms?: string[]; } export interface SignatureVerifierOptions { metadata?: MetadataInterface; keyFile?: string; signatureAlgorithm?: string; } export interface ExtractorResult { [key: string]: any; signature?: string | string[]; issuer?: string | string[]; nameID?: string; notexist?: boolean; } export interface LoginResponseAttribute { name: string; nameFormat: string; // valueXsiType: string; // valueTag: string; valueXmlnsXs?: string; valueXmlnsXsi?: string; } export interface LoginResponseAdditionalTemplates { attributeStatementTemplate?: AttributeStatementTemplate; attributeTemplate?: AttributeTemplate; } export interface BaseSamlTemplate { context: string; } export interface LoginResponseTemplate extends BaseSamlTemplate { attributes?: LoginResponseAttribute[]; additionalTemplates?: LoginResponseAdditionalTemplates; } export interface AttributeStatementTemplate extends BaseSamlTemplate { } export interface AttributeTemplate extends BaseSamlTemplate { } export interface LoginRequestTemplate extends BaseSamlTemplate { } export interface LogoutRequestTemplate extends BaseSamlTemplate { } export interface LogoutResponseTemplate extends BaseSamlTemplate { } export type KeyUse = 'signing' | 'encryption'; export interface KeyComponent { [key: string]: any; } export interface LibSamlInterface { getQueryParamByType: (type: string) => string; createXPath: (local, isExtractAll?: boolean) => string; replaceTagsByValue: (rawXML: string, tagValues: any) => string; attributeStatementBuilder: (attributes: LoginResponseAttribute[], attributeTemplate: AttributeTemplate, attributeStatementTemplate: AttributeStatementTemplate) => string; constructSAMLSignature: (opts: SignatureConstructor) => string; verifySignature: (xml: string, opts: SignatureVerifierOptions) => [boolean, any]; createKeySection: (use: KeyUse, cert: string | Buffer) => {}; constructMessageSignature: (octetString: string, key: string, passphrase?: string, isBase64?: boolean, signingAlgorithm?: string) => string; verifyMessageSignature: (metadata, octetString: string, signature: string | Buffer, verifyAlgorithm?: string) => boolean; getKeyInfo: (x509Certificate: string, signatureConfig?: any) => void; encryptAssertion: (sourceEntity, targetEntity, entireXML: string) => Promise<string>; decryptAssertion: (here, entireXML: string) => Promise<[string, any]>; getSigningScheme: (sigAlg: string) => string | null; getDigestMethod: (sigAlg: string) => string | null; nrsaAliasMapping: any; defaultLoginRequestTemplate: LoginRequestTemplate; defaultLoginResponseTemplate: LoginResponseTemplate; defaultAttributeStatementTemplate: AttributeStatementTemplate; defaultAttributeTemplate: AttributeTemplate; defaultLogoutRequestTemplate: LogoutRequestTemplate; defaultLogoutResponseTemplate: LogoutResponseTemplate; } const libSaml = () => { /** * @desc helper function to get back the query param for redirect binding for SLO/SSO * @type {string} */ function getQueryParamByType(type: string) { if ([urlParams.logoutRequest, urlParams.samlRequest].indexOf(type) !== -1) { return 'SAMLRequest'; } if ([urlParams.logoutResponse, urlParams.samlResponse].indexOf(type) !== -1) { return 'SAMLResponse'; } throw new Error('ERR_UNDEFINED_QUERY_PARAMS'); } /** * */ const nrsaAliasMapping = { 'http://www.w3.org/2000/09/xmldsig#rsa-sha1': 'pkcs1-sha1', 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256': 'pkcs1-sha256', 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512': 'pkcs1-sha512', }; /** * @desc Default login request template * @type {LoginRequestTemplate} */ const defaultLoginRequestTemplate = { context: '<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="{ID}" Version="2.0" IssueInstant="{IssueInstant}" Destination="{Destination}" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" AssertionConsumerServiceURL="{AssertionConsumerServiceURL}"><saml:Issuer>{Issuer}</saml:Issuer><samlp:NameIDPolicy Format="{NameIDFormat}" AllowCreate="{AllowCreate}"/></samlp:AuthnRequest>', }; /** * @desc Default logout request template * @type {LogoutRequestTemplate} */ const defaultLogoutRequestTemplate = { context: '<samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="{ID}" Version="2.0" IssueInstant="{IssueInstant}" Destination="{Destination}"><saml:Issuer>{Issuer}</saml:Issuer><saml:NameID Format="{NameIDFormat}">{NameID}</saml:NameID></samlp:LogoutRequest>', }; /** * @desc Default AttributeStatement template * @type {AttributeStatementTemplate} */ const defaultAttributeStatementTemplate = { context: '<saml:AttributeStatement>{Attributes}</saml:AttributeStatement>', }; /** * @desc Default Attribute template * @type {AttributeTemplate} */ const defaultAttributeTemplate = { context: '<saml:Attribute Name="{Name}" NameFormat="{NameFormat}"><saml:AttributeValue xmlns:xs="{ValueXmlnsXs}" xmlns:xsi="{ValueXmlnsXsi}" xsi:type="{ValueXsiType}">{Value}</saml:AttributeValue></saml:Attribute>', }; /** * @desc Default login response template * @type {LoginResponseTemplate} */ const defaultLoginResponseTemplate = { context: '<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="{ID}" Version="2.0" IssueInstant="{IssueInstant}" Destination="{Destination}" InResponseTo="{InResponseTo}"><saml:Issuer>{Issuer}</saml:Issuer><samlp:Status><samlp:StatusCode Value="{StatusCode}"/></samlp:Status><saml:Assertion xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="{AssertionID}" Version="2.0" IssueInstant="{IssueInstant}"><saml:Issuer>{Issuer}</saml:Issuer><saml:Subject><saml:NameID Format="{NameIDFormat}">{NameID}</saml:NameID><saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"><saml:SubjectConfirmationData NotOnOrAfter="{SubjectConfirmationDataNotOnOrAfter}" Recipient="{SubjectRecipient}" InResponseTo="{InResponseTo}"/></saml:SubjectConfirmation></saml:Subject><saml:Conditions NotBefore="{ConditionsNotBefore}" NotOnOrAfter="{ConditionsNotOnOrAfter}"><saml:AudienceRestriction><saml:Audience>{Audience}</saml:Audience></saml:AudienceRestriction></saml:Conditions>{AuthnStatement}{AttributeStatement}</saml:Assertion></samlp:Response>', attributes: [], additionalTemplates: { 'attributeStatementTemplate': defaultAttributeStatementTemplate, 'attributeTemplate': defaultAttributeTemplate } }; /** * @desc Default logout response template * @type {LogoutResponseTemplate} */ const defaultLogoutResponseTemplate = { context: '<samlp:LogoutResponse xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="{ID}" Version="2.0" IssueInstant="{IssueInstant}" Destination="{Destination}" InResponseTo="{InResponseTo}"><saml:Issuer>{Issuer}</saml:Issuer><samlp:Status><samlp:StatusCode Value="{StatusCode}"/></samlp:Status></samlp:LogoutResponse>', }; /** * @private * @desc Get the signing scheme alias by signature algorithms, used by the node-rsa module * @param {string} sigAlg signature algorithm * @return {string/null} signing algorithm short-hand for the module node-rsa */ function getSigningScheme(sigAlg?: string): SigningSchemeHash { if (sigAlg) { const algAlias = nrsaAliasMapping[sigAlg]; if (!(algAlias === undefined)) { return algAlias; } } return nrsaAliasMapping[signatureAlgorithms.RSA_SHA1]; } /** * @private * @desc Get the digest algorithms by signature algorithms * @param {string} sigAlg signature algorithm * @return {string/null} digest algorithm */ function getDigestMethod(sigAlg: string): string | null { const digestAlg = digestAlgorithms[sigAlg]; if (!(digestAlg === undefined)) { return digestAlg; } return null; // default value } /** * @public * @desc Create XPath * @param {string/object} local parameters to create XPath * @param {boolean} isExtractAll define whether returns whole content according to the XPath * @return {string} xpath */ function createXPath(local, isExtractAll?: boolean): string { if (isString(local)) { return isExtractAll === true ? "//*[local-name(.)='" + local + "']/text()" : "//*[local-name(.)='" + local + "']"; } return "//*[local-name(.)='" + local.name + "']/@" + local.attr; } /** * @private * @desc Tag normalization * @param {string} prefix prefix of the tag * @param {content} content normalize it to capitalized camel case * @return {string} */ function tagging(prefix: string, content: string): string { const camelContent = camelCase(content, {locale: 'en-us'}); return prefix + camelContent.charAt(0).toUpperCase() + camelContent.slice(1); } function escapeTag(replacement: unknown): (...args: string[]) => string { return (_match: string, quote?: string) => { const text: string = (replacement === null || replacement === undefined) ? '' : String(replacement); // not having a quote means this interpolation isn't for an attribute, and so does not need escaping return quote ? `${quote}${xmlEscape(text)}` : text; } } return { createXPath, getQueryParamByType, defaultLoginRequestTemplate, defaultLoginResponseTemplate, defaultAttributeStatementTemplate, defaultAttributeTemplate, defaultLogoutRequestTemplate, defaultLogoutResponseTemplate, /** * @desc Replace the tag (e.g. {tag}) inside the raw XML * @param {string} rawXML raw XML string used to do keyword replacement * @param {array} tagValues tag values * @return {string} */ replaceTagsByValue(rawXML: string, tagValues: Record<string, unknown>): string { Object.keys(tagValues).forEach(t => { rawXML = rawXML.replace( new RegExp(`("?)\\{${t}\\}`, 'g'), escapeTag(tagValues[t]) ); }); return rawXML; }, /** * @desc Helper function to build the AttributeStatement tag * @param {LoginResponseAttribute} attributes an array of attribute configuration * @param {AttributeTemplate} attributeTemplate the attribute tag template to be used * @param {AttributeStatementTemplate} attributeStatementTemplate the attributeStatement tag template to be used * @return {string} */ attributeStatementBuilder( attributes: LoginResponseAttribute[], attributeTemplate: AttributeTemplate = defaultAttributeTemplate, attributeStatementTemplate: AttributeStatementTemplate = defaultAttributeStatementTemplate ): string { const attr = attributes.map(({ name, nameFormat, valueTag, valueXsiType, valueXmlnsXs, valueXmlnsXsi }) => { const defaultValueXmlnsXs = 'http://www.w3.org/2001/XMLSchema'; const defaultValueXmlnsXsi = 'http://www.w3.org/2001/XMLSchema-instance'; let attributeLine = attributeTemplate.context; attributeLine = attributeLine.replace('{Name}', name); attributeLine = attributeLine.replace('{NameFormat}', nameFormat); attributeLine = attributeLine.replace('{ValueXmlnsXs}', valueXmlnsXs ? valueXmlnsXs : defaultValueXmlnsXs); attributeLine = attributeLine.replace('{ValueXmlnsXsi}', valueXmlnsXsi ? valueXmlnsXsi : defaultValueXmlnsXsi); attributeLine = attributeLine.replace('{ValueXsiType}', valueXsiType); attributeLine = attributeLine.replace('{Value}', `{${tagging('attr', valueTag)}}`); return attributeLine; }).join(''); return attributeStatementTemplate.context.replace('{Attributes}', attr); }, /** * @desc Construct the XML signature for POST binding * @param {string} rawSamlMessage request/response xml string * @param {string} referenceTagXPath reference uri * @param {string} privateKey declares the private key * @param {string} passphrase passphrase of the private key [optional] * @param {string|buffer} signingCert signing certificate * @param {string} signatureAlgorithm signature algorithm * @param {string[]} transformationAlgorithms canonicalization and transformation Algorithms * @return {string} base64 encoded string */ constructSAMLSignature(opts: SignatureConstructor) { const { rawSamlMessage, referenceTagXPath, privateKey, privateKeyPass, signatureAlgorithm = signatureAlgorithms.RSA_SHA256, transformationAlgorithms = [ 'http://www.w3.org/2000/09/xmldsig#enveloped-signature', 'http://www.w3.org/2001/10/xml-exc-c14n#', ], signingCert, signatureConfig, isBase64Output = true, isMessageSigned = false, } = opts; const sig = new SignedXml(); // Add assertion sections as reference if (referenceTagXPath) { sig.addReference( referenceTagXPath, transformationAlgorithms, // @ts-ignore getDigestMethod(signatureAlgorithm) ); } if (isMessageSigned) { sig.addReference( // reference to the root node '/*', transformationAlgorithms, // @ts-ignore getDigestMethod(signatureAlgorithm), '', '', '', false, ); } sig.signatureAlgorithm = signatureAlgorithm; sig.keyInfoProvider = new this.getKeyInfo(signingCert, signatureConfig); sig.signingKey = utility.readPrivateKey(privateKey, privateKeyPass, true); if (signatureConfig) { sig.computeSignature(rawSamlMessage, signatureConfig); } else { sig.computeSignature(rawSamlMessage); } return isBase64Output !== false ? utility.base64Encode(sig.getSignedXml()) : sig.getSignedXml(); }, /** * @desc Verify the XML signature * @param {string} xml xml * @param {SignatureVerifierOptions} opts cert declares the X509 certificate * @return {boolean} verification result */ verifySignature(xml: string, opts: SignatureVerifierOptions) { const { dom } = getContext(); const doc = dom.parseFromString(xml); // In order to avoid the wrapping attack, we have changed to use absolute xpath instead of naively fetching the signature element // message signature (logout response / saml response) const messageSignatureXpath = "/*[contains(local-name(), 'Response') or contains(local-name(), 'Request')]/*[local-name(.)='Signature']"; // assertion signature (logout response / saml response) const assertionSignatureXpath = "/*[contains(local-name(), 'Response') or contains(local-name(), 'Request')]/*[local-name(.)='Assertion']/*[local-name(.)='Signature']"; // check if there is a potential malicious wrapping signature const wrappingElementsXPath = "/*[contains(local-name(), 'Response')]/*[local-name(.)='Assertion']/*[local-name(.)='Subject']/*[local-name(.)='SubjectConfirmation']/*[local-name(.)='SubjectConfirmationData']//*[local-name(.)='Assertion' or local-name(.)='Signature']"; // select the signature node let selection: any = []; let assertionNode: string | null = null; const messageSignatureNode = select(messageSignatureXpath, doc); const assertionSignatureNode = select(assertionSignatureXpath, doc); const wrappingElementNode = select(wrappingElementsXPath, doc); selection = selection.concat(messageSignatureNode); selection = selection.concat(assertionSignatureNode); // try to catch potential wrapping attack if (wrappingElementNode.length !== 0) { throw new Error('ERR_POTENTIAL_WRAPPING_ATTACK'); } // guarantee to have a signature in saml response if (selection.length === 0) { throw new Error('ERR_ZERO_SIGNATURE'); } const sig = new SignedXml(); let verified = true; // need to refactor later on selection.forEach(signatureNode => { // @ts-ignore sig.signatureAlgorithm = opts.signatureAlgorithm; if (!opts.keyFile && !opts.metadata) { throw new Error('ERR_UNDEFINED_SIGNATURE_VERIFIER_OPTIONS'); } if (opts.keyFile) { sig.keyInfoProvider = new FileKeyInfo(opts.keyFile); } if (opts.metadata) { const certificateNode = select(".//*[local-name(.)='X509Certificate']", signatureNode) as any; // certificate in metadata let metadataCert: any = opts.metadata.getX509Certificate(certUse.signing); // flattens the nested array of Certificates from each KeyDescriptor if (Array.isArray(metadataCert)) { metadataCert = flattenDeep(metadataCert); } else if (typeof metadataCert === 'string') { metadataCert = [metadataCert]; } // normalise the certificate string metadataCert = metadataCert.map(utility.normalizeCerString); // no certificate in node response nor metadata if (certificateNode.length === 0 && metadataCert.length === 0) { throw new Error('NO_SELECTED_CERTIFICATE'); } // certificate node in response if (certificateNode.length !== 0) { const x509CertificateData = certificateNode[0].firstChild.data; const x509Certificate = utility.normalizeCerString(x509CertificateData); if ( metadataCert.length >= 1 && !metadataCert.find(cert => cert.trim() === x509Certificate.trim()) ) { // keep this restriction for rolling certificate usage // to make sure the response certificate is one of those specified in metadata throw new Error('ERROR_UNMATCH_CERTIFICATE_DECLARATION_IN_METADATA'); } sig.keyInfoProvider = new this.getKeyInfo(x509Certificate); } else { // Select first one from metadata sig.keyInfoProvider = new this.getKeyInfo(metadataCert[0]); } } sig.loadSignature(signatureNode); doc.removeChild(signatureNode); verified = verified && sig.checkSignature(doc.toString()); // immediately throw error when any one of the signature is failed to get verified if (!verified) { throw new Error('ERR_FAILED_TO_VERIFY_SIGNATURE'); } }); // response must be signed, either entire document or assertion // default we will take the assertion section under root if (messageSignatureNode.length === 1) { const node = select("/*[contains(local-name(), 'Response') or contains(local-name(), 'Request')]/*[local-name(.)='Assertion']", doc); if (node.length === 1) { assertionNode = node[0].toString(); } } if (assertionSignatureNode.length === 1) { const verifiedAssertionInfo = extract(assertionSignatureNode[0].toString(), [{ key: 'refURI', localPath: ['Signature', 'SignedInfo', 'Reference'], attributes: ['URI'] }]); // get the assertion supposed to be the one should be verified const desiredAssertionInfo = extract(doc.toString(), [{ key: 'id', localPath: ['~Response', 'Assertion'], attributes: ['ID'] }]); // 5.4.2 References // SAML assertions and protocol messages MUST supply a value for the ID attribute on the root element of // the assertion or protocol message being signed. The assertion’s or protocol message's root element may // or may not be the root element of the actual XML document containing the signed assertion or protocol // message (e.g., it might be contained within a SOAP envelope). // Signatures MUST contain a single <ds:Reference> containing a same-document reference to the ID // attribute value of the root element of the assertion or protocol message being signed. For example, if the // ID attribute value is "foo", then the URI attribute in the <ds:Reference> element MUST be "#foo". if (verifiedAssertionInfo.refURI !== `#${desiredAssertionInfo.id}`) { throw new Error('ERR_POTENTIAL_WRAPPING_ATTACK'); } const verifiedDoc = extract(doc.toString(), [{ key: 'assertion', localPath: ['~Response', 'Assertion'], attributes: [], context: true }]); assertionNode = verifiedDoc.assertion.toString(); } return [verified, assertionNode]; }, /** * @desc Helper function to create the key section in metadata (abstraction for signing and encrypt use) * @param {string} use type of certificate (e.g. signing, encrypt) * @param {string} certString declares the certificate String * @return {object} object used in xml module */ createKeySection(use: KeyUse, certString: string | Buffer): KeyComponent { return { ['KeyDescriptor']: [ { _attr: { use }, }, { ['ds:KeyInfo']: [ { _attr: { 'xmlns:ds': 'http://www.w3.org/2000/09/xmldsig#', }, }, { ['ds:X509Data']: [{ 'ds:X509Certificate': utility.normalizeCerString(certString), }], }, ], }], }; }, /** * @desc Constructs SAML message * @param {string} octetString see "Bindings for the OASIS Security Assertion Markup Language (SAML V2.0)" P.17/46 * @param {string} key declares the pem-formatted private key * @param {string} passphrase passphrase of private key [optional] * @param {string} signingAlgorithm signing algorithm * @return {string} message signature */ constructMessageSignature( octetString: string, key: string, passphrase?: string, isBase64?: boolean, signingAlgorithm?: string ) { // Default returning base64 encoded signature // Embed with node-rsa module const decryptedKey = new nrsa( utility.readPrivateKey(key, passphrase), undefined, { signingScheme: getSigningScheme(signingAlgorithm), } ); const signature = decryptedKey.sign(octetString); // Use private key to sign data return isBase64 !== false ? signature.toString('base64') : signature; }, /** * @desc Verifies message signature * @param {Metadata} metadata metadata object of identity provider or service provider * @param {string} octetString see "Bindings for the OASIS Security Assertion Markup Language (SAML V2.0)" P.17/46 * @param {string} signature context of XML signature * @param {string} verifyAlgorithm algorithm used to verify * @return {boolean} verification result */ verifyMessageSignature( metadata, octetString: string, signature: string | Buffer, verifyAlgorithm?: string ) { const signCert = metadata.getX509Certificate(certUse.signing); const signingScheme = getSigningScheme(verifyAlgorithm); const key = new nrsa(utility.getPublicKeyPemFromCertificate(signCert), 'public', { signingScheme }); return key.verify(Buffer.from(octetString), Buffer.from(signature)); }, /** * @desc Get the public key in string format * @param {string} x509Certificate certificate * @return {string} public key */ getKeyInfo(x509Certificate: string, signatureConfig: any = {}) { this.getKeyInfo = key => { const prefix = signatureConfig.prefix ? `${signatureConfig.prefix}:` : ''; return `<${prefix}X509Data><${prefix}X509Certificate>${x509Certificate}</${prefix}X509Certificate></${prefix}X509Data>`; }; this.getKey = keyInfo => { return utility.getPublicKeyPemFromCertificate(x509Certificate).toString(); }; }, /** * @desc Encrypt the assertion section in Response * @param {Entity} sourceEntity source entity * @param {Entity} targetEntity target entity * @param {string} xml response in xml string format * @return {Promise} a promise to resolve the finalized xml */ encryptAssertion(sourceEntity, targetEntity, xml?: string) { // Implement encryption after signature if it has return new Promise<string>((resolve, reject) => { if (!xml) { return reject(new Error('ERR_UNDEFINED_ASSERTION')); } const sourceEntitySetting = sourceEntity.entitySetting; const targetEntityMetadata = targetEntity.entityMeta; const { dom } = getContext(); const doc = dom.parseFromString(xml); const assertions = select("//*[local-name(.)='Assertion']", doc) as Node[]; if (!Array.isArray(assertions) || assertions.length === 0) { throw new Error('ERR_NO_ASSERTION'); } if (assertions.length > 1) { throw new Error('ERR_MULTIPLE_ASSERTION'); } const rawAssertionNode = assertions[0]; // Perform encryption depends on the setting, default is false if (sourceEntitySetting.isAssertionEncrypted) { const publicKeyPem = utility.getPublicKeyPemFromCertificate(targetEntityMetadata.getX509Certificate(certUse.encrypt)); xmlenc.encrypt(rawAssertionNode.toString(), { // use xml-encryption module rsa_pub: Buffer.from(publicKeyPem), // public key from certificate pem: Buffer.from(`-----BEGIN CERTIFICATE-----${targetEntityMetadata.getX509Certificate(certUse.encrypt)}-----END CERTIFICATE-----`), encryptionAlgorithm: sourceEntitySetting.dataEncryptionAlgorithm, keyEncryptionAlgorithm: sourceEntitySetting.keyEncryptionAlgorithm, }, (err, res) => { if (err) { console.error(err); return reject(new Error('ERR_EXCEPTION_OF_ASSERTION_ENCRYPTION')); } if (!res) { return reject(new Error('ERR_UNDEFINED_ENCRYPTED_ASSERTION')); } const { encryptedAssertion: encAssertionPrefix } = sourceEntitySetting.tagPrefix; const encryptAssertionDoc = dom.parseFromString(`<${encAssertionPrefix}:EncryptedAssertion xmlns:${encAssertionPrefix}="${namespace.names.assertion}">${res}</${encAssertionPrefix}:EncryptedAssertion>`); doc.documentElement.replaceChild(encryptAssertionDoc.documentElement, rawAssertionNode); return resolve(utility.base64Encode(doc.toString())); }); } else { return resolve(utility.base64Encode(xml)); // No need to do encryption } }); }, /** * @desc Decrypt the assertion section in Response * @param {string} type only accept SAMLResponse to proceed decryption * @param {Entity} here this entity * @param {Entity} from from the entity where the message is sent * @param {string} entireXML response in xml string format * @return {function} a promise to get back the entire xml with decrypted assertion */ decryptAssertion(here, entireXML: string) { return new Promise<[string, any]>((resolve, reject) => { // Implement decryption first then check the signature if (!entireXML) { return reject(new Error('ERR_UNDEFINED_ASSERTION')); } // Perform encryption depends on the setting of where the message is sent, default is false const hereSetting = here.entitySetting; const { dom } = getContext(); const doc = dom.parseFromString(entireXML); const encryptedAssertions = select("/*[contains(local-name(), 'Response')]/*[local-name(.)='EncryptedAssertion']", doc) as Node[]; if (!Array.isArray(encryptedAssertions) || encryptedAssertions.length === 0) { throw new Error('ERR_UNDEFINED_ENCRYPTED_ASSERTION'); } if (encryptedAssertions.length > 1) { throw new Error('ERR_MULTIPLE_ASSERTION'); } const encAssertionNode = encryptedAssertions[0]; return xmlenc.decrypt(encAssertionNode.toString(), { key: utility.readPrivateKey(hereSetting.encPrivateKey, hereSetting.encPrivateKeyPass), }, (err, res) => { if (err) { console.error(err); return reject(new Error('ERR_EXCEPTION_OF_ASSERTION_DECRYPTION')); } if (!res) { return reject(new Error('ERR_UNDEFINED_ENCRYPTED_ASSERTION')); } const rawAssertionDoc = dom.parseFromString(res); doc.documentElement.replaceChild(rawAssertionDoc.documentElement, encAssertionNode); return resolve([doc.toString(), res]); }); }); }, /** * @desc Check if the xml string is valid and bounded */ async isValidXml(input: string) { // check if global api contains the validate function const { validate } = getContext(); /** * user can write a validate function that always returns * a resolved promise and skip the validator even in * production, user will take the responsibility if * they intend to skip the validation */ if (!validate) { // otherwise, an error will be thrown return Promise.reject('Your application is potentially vulnerable because no validation function found. Please read the documentation on how to setup the validator. (https://github.com/tngan/samlify#installation)'); } try { return await validate(input); } catch (e) { throw e; } }, }; }; export default libSaml();