Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/cert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const stripCertHeaderAndFooter = (cert: string): string => {
cert = cert.replace(/-+BEGIN CERTIFICATE-+\r?\n?/, '');
cert = cert.replace(/-+END CERTIFICATE-+\r?\n?/, '');
cert = cert.replace(/\r\n/g, '\n');
cert = cert.trim().replace(/\s+/g, '');
return cert.trimEnd();
};

Expand Down
36 changes: 20 additions & 16 deletions lib/decrypt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,33 @@ import { select } from 'xpath';
import * as xmlenc from 'xml-encryption';
import { parseFromString } from './utils';

const assertion = (xml: Document, encryptedAssertions: Node[], options) => {
const decryptAssertions = (xml: Document, encryptedAssertions: Node[], options) => {
if (!Array.isArray(encryptedAssertions)) {
throw new Error('Undefined Encrypted Assertion.');
}
if (encryptedAssertions.length !== 1) {
throw new Error('Multiple Assertion.');
}

return xmlenc.decrypt(encryptedAssertions[0], { key: options.privateKey }, (err, res) => {
if (err) {
throw new Error('Exception of Assertion Decryption.');
return xmlenc.decrypt(
(encryptedAssertions[0] as Node).toString(),
{ key: options.privateKey },
(err, res) => {
if (err) {
throw new Error('Exception of Assertion Decryption.');
}
if (!res) {
throw new Error('Undefined Encryption Assertion.');
}

const assertionNode = parseFromString(res);
xml.documentElement.removeChild(encryptedAssertions[0]);
// @ts-expect-error missing Node properties are not needed
xml.documentElement.appendChild(assertionNode!.childNodes[0]);

return { assertion: xml.toString(), decrypted: true };
}
if (!res) {
throw new Error('Undefined Encryption Assertion.');
}

const assertionNode = parseFromString(res);
xml.documentElement.removeChild(encryptedAssertions[0]);
// @ts-expect-error missing Node properties are not needed
xml.documentElement.appendChild(assertionNode!.childNodes[0]);

return { assertion: xml.toString(), decrypted: true };
});
);
};
const decryptXml = (entireXML: string, options): { assertion: string; decrypted: boolean } => {
if (!entireXML) {
Expand All @@ -41,7 +45,7 @@ const decryptXml = (entireXML: string, options): { assertion: string; decrypted:

if (encryptedAssertions.length >= 1) {
// @ts-expect-error missing Node properties are not needed
return assertion(xml!, encryptedAssertions, options);
return decryptAssertions(xml!, encryptedAssertions, options);
}

return { assertion: entireXML, decrypted: false };
Expand Down
86 changes: 86 additions & 0 deletions lib/encrypt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import * as xmlenc from 'xml-encryption';
import { certToPEM } from './validateSignature';
import { EncryptOptions } from './typings';

/**
* Enumeration of standard XML Encryption algorithms used in SAML 2.0.
*/
export const EncryptionAlgorithms = {
/**
* Block encryption algorithm (AES-128 in CBC mode).
*/
AES128_CBC: 'http://www.w3.org/2001/04/xmlenc#aes128-cbc',

/**
* Block encryption algorithm (AES-256 in CBC mode).
* @description Standard algorithm often used in legacy SAML implementations.
*/
AES256_CBC: 'http://www.w3.org/2001/04/xmlenc#aes256-cbc',

/**
* Authenticated encryption algorithm (AES-256 in GCM mode).
* @description Recommended for modern implementations (requires Node.js 10+).
* Provides both confidentiality and data integrity.
*/
AES256_GCM: 'http://www.w3.org/2009/xmlenc11#aes256-gcm',

/**
* Key transport algorithm (RSA-OAEP with MGF1).
* @description The standard algorithm for encrypting the symmetric key using the SP's public key.
*/
RSA_OAEP_MGF1P: 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p',
};

/**
* Encrypts a raw SAML Assertion XML string according to SAML 2.0 standards.
* * @param {string} rawAssertion - The signed or raw XML Assertion string to be encrypted.
* @param {EncryptOptions} options - Configuration object containing the public key (raw string) and algorithms.
* @returns {Promise<string>} A promise that resolves to the fully encrypted XML string wrapped in <saml:EncryptedAssertion>.
*/
export const encryptAssertion = (rawAssertion: string, options: EncryptOptions): Promise<string> => {
return new Promise((resolve, reject) => {
try {
// Ensure the public key is in valid PEM format before passing to the library
const validPemKey = certToPEM(options.publicKey);

const encAlgo = options.encryptionAlgorithm || EncryptionAlgorithms.AES256_CBC;
const keyAlgo = options.keyEncryptionAlgorithm || EncryptionAlgorithms.RSA_OAEP_MGF1P;

const encryptOptions: any = {

Check warning on line 49 in lib/encrypt.ts

View workflow job for this annotation

GitHub Actions / build (24)

Unexpected any. Specify a different type
rsa_pub: validPemKey,
pem: validPemKey,
encryptionAlgorithm: encAlgo,
keyEncryptionAlgorithm: keyAlgo,
disig: true, // Adds <ds:KeyInfo> structure
warn: false,
...options,
};

xmlenc.encrypt(rawAssertion, encryptOptions, (err, result) => {
if (err) {
return reject(new Error(`SAML Encryption failed: ${err.message}`));
}

/**
* The xml-encryption library returns the <xenc:EncryptedData> element.
* According to the SAML XSD, this must be wrapped inside a <saml:EncryptedAssertion> element.
* * Structure:
* <saml:EncryptedAssertion>
* <xenc:EncryptedData>
* <xenc:EncryptionMethod />
* <ds:KeyInfo>
* <xenc:EncryptedKey> ... </xenc:EncryptedKey>
* </ds:KeyInfo>
* <xenc:CipherData> ... </xenc:CipherData>
* </xenc:EncryptedData>
* </saml:EncryptedAssertion>
*/
const wrappedAssertion = `<saml:EncryptedAssertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">${result}</saml:EncryptedAssertion>`;

resolve(wrappedAssertion.trim());
});
} catch (error: any) {

Check warning on line 82 in lib/encrypt.ts

View workflow job for this annotation

GitHub Actions / build (24)

Unexpected any. Specify a different type
reject(new Error(`Pre-encryption setup failed: ${error.message}`));
}
});
};
14 changes: 14 additions & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { createPostForm } from './post';
import { sign } from './sign';
import { decryptXml } from './decrypt';
import { parseLogoutResponse, createLogoutRequest } from './logout';
import { encryptAssertion, EncryptionAlgorithms } from './encrypt';
import {EncryptOptions, SpMetadataOptions, IdpMetadataOptions, SAMLResponseOptions, SAMLReq, SAMLProfile, SignOptions} from './typings';

export default {
parseMetadata,
Expand All @@ -32,4 +34,16 @@ export default {
WrapError,
parseLogoutResponse,
createLogoutRequest,
encryptAssertion,
EncryptionAlgorithms,
};

export type {
EncryptOptions,
SpMetadataOptions,
IdpMetadataOptions,
SAMLResponseOptions,
SAMLReq,
SAMLProfile,
SignOptions
};
115 changes: 65 additions & 50 deletions lib/metadata.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { getAttribute } from './utils';
import { thumbprint } from './utils';
import { stripCertHeaderAndFooter } from './cert';
import { EncryptionAlgorithms } from './encrypt';

import crypto from 'crypto';
import xml2js from 'xml2js';
import xmlbuilder from 'xmlbuilder';
import { IdpMetadataOptions, SpMetadataOptions } from './typings';

const BEGIN = '-----BEGIN CERTIFICATE-----';
const END = '-----END CERTIFICATE-----';

const parseMetadata = async (idpMeta: string, validateOpts): Promise<Record<string, any>> => {

Check warning on line 14 in lib/metadata.ts

View workflow job for this annotation

GitHub Actions / build (24)

Unexpected any. Specify a different type
return new Promise((resolve, reject) => {
// Some Providers do not escape the & character in the metadata, for now these have been encountered in errorURL
idpMeta = idpMeta.replace(/errorURL=".*?"/g, '');
Expand All @@ -34,7 +36,7 @@
let sloRedirectUrl: null | undefined = null;
let sloPostUrl: null | undefined = null;

let ssoDes: any = getAttribute(res, 'EntityDescriptor.IDPSSODescriptor', null);

Check warning on line 39 in lib/metadata.ts

View workflow job for this annotation

GitHub Actions / build (24)

Unexpected any. Specify a different type
if (!ssoDes) {
ssoDes = getAttribute(res, 'EntityDescriptor.SPSSODescriptor', []);
if (ssoDes.length > 0) {
Expand Down Expand Up @@ -105,7 +107,7 @@
}
}

const ret: Record<string, any> = {

Check warning on line 110 in lib/metadata.ts

View workflow job for this annotation

GitHub Actions / build (24)

Unexpected any. Specify a different type
sso: {},
slo: {},
};
Expand Down Expand Up @@ -186,80 +188,93 @@
});
};

const createIdPMetadataXML = ({
ssoUrl,
entityId,
x509cert,
wantAuthnRequestsSigned,
}: {
ssoUrl: string;
entityId: string;
x509cert: string;
wantAuthnRequestsSigned: boolean;
}): string => {
x509cert = stripCertHeaderAndFooter(x509cert);
const createIdPMetadataXML = (options: IdpMetadataOptions): string => {
const validSigningCert = stripCertHeaderAndFooter(options.signingCert);
const validEncryptionCert = options.encryption
? options.encryptionCert
? stripCertHeaderAndFooter(options.encryptionCert)
: stripCertHeaderAndFooter(options.signingCert)
: null;

const keyDescriptors: any[] = [];

Check warning on line 199 in lib/metadata.ts

View workflow job for this annotation

GitHub Actions / build (24)

Unexpected any. Specify a different type

keyDescriptors.push({
'@use': 'signing',
'ds:KeyInfo': {
'ds:X509Data': {
'ds:X509Certificate': validSigningCert,
},
},
});

if (validEncryptionCert) {
keyDescriptors.push({
'@use': 'encryption',
'ds:KeyInfo': {
'ds:X509Data': {
'ds:X509Certificate': validEncryptionCert,
},
},
EncryptionMethod: [
{ '@Algorithm': EncryptionAlgorithms.AES256_CBC },
{ '@Algorithm': EncryptionAlgorithms.AES128_CBC },
{ '@Algorithm': EncryptionAlgorithms.AES256_GCM },
{ '@Algorithm': EncryptionAlgorithms.RSA_OAEP_MGF1P },
],
});
}

const nameIDFormats = options.nameIDFormat || [
'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'
];

const today = new Date();
const nodes = {
'md:EntityDescriptor': {
'@xmlns:md': 'urn:oasis:names:tc:SAML:2.0:metadata',
'@entityID': entityId,
'@validUntil': new Date(today.setFullYear(today.getFullYear() + 10)).toISOString(),
'md:IDPSSODescriptor': {
'@WantAuthnRequestsSigned': wantAuthnRequestsSigned,
EntityDescriptor: {
'@xmlns': 'urn:oasis:names:tc:SAML:2.0:metadata',
'@xmlns:ds': 'http://www.w3.org/2000/09/xmldsig#',
'@EntityID': options.entityID,
...(options.validUntil && { '@validUntil': options.validUntil.toISOString() }),

IDPSSODescriptor: {
'@protocolSupportEnumeration': 'urn:oasis:names:tc:SAML:2.0:protocol',
'md:KeyDescriptor': {
'@use': 'signing',
'ds:KeyInfo': {
'@xmlns:ds': 'http://www.w3.org/2000/09/xmldsig#',
'ds:X509Data': {
'ds:X509Certificate': {
'#text': x509cert,
},
},
},
},
'md:NameIDFormat': {
'#text': 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
},
'md:SingleSignOnService': [
KeyDescriptor: keyDescriptors,
NameIDFormat: nameIDFormats,
SingleSignOnService: [
{
'@Binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
'@Location': ssoUrl,
'@Location': options.ssoUrl,
},
{
'@Binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',
'@Location': ssoUrl,
'@Location': options.ssoUrl,
},
],
},
},
};

if (options.sloUrl) {
(nodes.EntityDescriptor.IDPSSODescriptor as any).SingleLogoutService = {

Check warning on line 257 in lib/metadata.ts

View workflow job for this annotation

GitHub Actions / build (24)

Unexpected any. Specify a different type
'@Binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
'@Location': options.sloUrl,
};
}

return xmlbuilder.create(nodes, { encoding: 'UTF-8', standalone: false }).end({ pretty: true });
};

const createSPMetadataXML = ({
entityId,
publicKeyString,
acsUrl,
encryption,
}: {
entityId: string;
publicKeyString: string;
acsUrl: string;
encryption: boolean;
}): string => {
const createSPMetadataXML = (options: SpMetadataOptions): string => {
const {entityID, publicKey, acsUrl, encryption} = options;
const today = new Date();

const keyDescriptor: any[] = [

Check warning on line 270 in lib/metadata.ts

View workflow job for this annotation

GitHub Actions / build (24)

Unexpected any. Specify a different type
{
'@use': 'signing',
'ds:KeyInfo': {
'@xmlns:ds': 'http://www.w3.org/2000/09/xmldsig#',
'ds:X509Data': {
'ds:X509Certificate': {
'#text': publicKeyString,
'#text': publicKey,
},
},
},
Expand All @@ -273,7 +288,7 @@
'@xmlns:ds': 'http://www.w3.org/2000/09/xmldsig#',
'ds:X509Data': {
'ds:X509Certificate': {
'#text': publicKeyString,
'#text': publicKey,
},
},
},
Expand All @@ -286,7 +301,7 @@
const nodes = {
'md:EntityDescriptor': {
'@xmlns:md': 'urn:oasis:names:tc:SAML:2.0:metadata',
'@entityID': entityId,
'@entityID': entityID,
'@validUntil': new Date(today.setFullYear(today.getFullYear() + 10)).toISOString(),
'md:SPSSODescriptor': {
//'@WantAuthnRequestsSigned': true,
Expand Down
2 changes: 1 addition & 1 deletion lib/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ const request = ({

let xml = xmlbuilder.create(samlReq).end({});
if (signingKey) {
xml = sign(xml, signingKey, publicKey, authnXPath);
xml = sign(xml, {privateKey: signingKey, publicKey, sigLocation: authnXPath});
}

return {
Expand Down
Loading
Loading