From a3e71bdd2e960ac3dbb5ba02f3b09bd3fb30bb1a Mon Sep 17 00:00:00 2001 From: Deepak Prabhakara Date: Wed, 19 Nov 2025 10:11:18 +0000 Subject: [PATCH 1/3] added missing tests --- test/lib/saml20.spec.ts | 28 +++++++++++++++++ test/lib/sign.spec.ts | 23 ++++++++++++++ test/lib/utils.spec.ts | 69 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 120 insertions(+) create mode 100644 test/lib/sign.spec.ts create mode 100644 test/lib/utils.spec.ts diff --git a/test/lib/saml20.spec.ts b/test/lib/saml20.spec.ts index 754019e1..da581193 100644 --- a/test/lib/saml20.spec.ts +++ b/test/lib/saml20.spec.ts @@ -129,4 +129,32 @@ describe('saml20.ts', function () { assert(error); } }); + it('getClaims with friendly names', function () { + const attributes = [ + { + '@': { Name: 'name', FriendlyName: 'email' }, + 'AttributeValue': { _: 'test@example.com' } + }, + { + '@': { Name: 'givenName', FriendlyName: 'givenName' }, + 'AttributeValue': { _: 'John' } + }, + { + '@': { Name: 'surname', FriendlyName: 'sn' }, + 'AttributeValue': { _: 'Doe' } + } + ]; + + const assertionWithFriendlyNames = { + AttributeStatement: { + Attribute: attributes + } + }; + + const result = saml20.parse(assertionWithFriendlyNames); + const claims = result.claims as any; + assert.strictEqual(claims['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'], 'test@example.com'); + assert.strictEqual(claims['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname'], 'John'); + assert.strictEqual(claims['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname'], 'Doe'); + }); }); diff --git a/test/lib/sign.spec.ts b/test/lib/sign.spec.ts new file mode 100644 index 00000000..0fc74adc --- /dev/null +++ b/test/lib/sign.spec.ts @@ -0,0 +1,23 @@ +import assert from 'assert'; +import { sign } from '../../lib/sign'; +import fs from 'fs'; + +const validXml = 'me'; +const signingKey = fs.readFileSync('./test/assets/certificates/oktaPrivateKey.pem').toString(); +const publicKey = fs.readFileSync('./test/assets/certificates/oktaPublicKey.crt').toString(); + +describe('sign.ts', function () { + it('should sign valid XML', function () { + const signed = sign(validXml, signingKey, publicKey, '/*[local-name(.)="Assertion"]'); + assert(signed); + assert(signed.includes('Signature')); + }); + + it('should throw error if xml is missing', function () { + assert.throws(() => sign('', signingKey, publicKey, ''), /Please specify xml/); + }); + + it('should throw error if signingKey is missing', function () { + assert.throws(() => sign(validXml, '', publicKey, ''), /Please specify signingKey/); + }); +}); diff --git a/test/lib/utils.spec.ts b/test/lib/utils.spec.ts new file mode 100644 index 00000000..044ddb23 --- /dev/null +++ b/test/lib/utils.spec.ts @@ -0,0 +1,69 @@ +import assert from 'assert'; +import { parseFromString, thumbprint, getAttribute, isMultiRootedXMLError, multiRootedXMLError } from '../../lib/utils'; + +describe('utils.ts', function () { + describe('parseFromString', function () { + it('should parse valid XML', function () { + const xml = 'test'; + const doc = parseFromString(xml); + assert(doc); + assert(doc.documentElement); + assert.strictEqual(doc.documentElement.nodeName, 'root'); + }); + + it('should throw error for invalid XML', function () { + assert.throws(() => parseFromString('unclosed'), /unclosed xml tag/); + }); + + it('should throw error for multi-rooted XML', function () { + assert.throws(() => parseFromString(''), /multirooted xml not allowed/); + }); + + it('should throw error for empty XML', function () { + assert.throws(() => parseFromString(''), /missing root element/); + }); + }); + + describe('thumbprint', function () { + it('should calculate correct thumbprint', function () { + const cert = 'MIICajCCAdOgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBSMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRcwFQYDVQQDDA5zcC5leGFtcGxlLmNvbTAeFw0xNDA3MTcxNDEyNTZaFw0xNTA3MTcxNDEyNTZaMFIxCzAJBgNVBAYTAnVzMRMwEQYDVQQIDApDYWxpZm9ybmlhMRUwEwYDVQQKDAxPbmVsb2dpbiBJbmMxFzAVBgNVBAMMDnNwLmV4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDZx+ON4IUoIWxgukTb1tOiX3bMYzYQiwWPUNMp+Fq82xoNogso2bykZG0yiJm5o8zv/sd6pGouayMgkx/2FSOdc36T0jGbCHuRSbtia0PEzNIRtmViMrt3AeoWBidRXmZsxCNLwgIV6dn2WpuE5Az0bHgpZnQxTKFek0BMKU/d8wIDAQABo1AwTjAdBgNVHQ4EFgQUGHxYqZYyX7cTxKVODVgZwSTdCnwwHwYDVR0jBBgwFoAUGHxYqZYyX7cTxKVODVgZwSTdCnwwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOBgQByFOl+hMFICbd3DJfnp2Rgd/dqttsZG/tyhILWvErbio/DEe98mXpowhTkC04ENprOyXi7ZbUqiicF89uAGyt1oqgTUCD1VsLahqIcmrzgumNyTwLGWo17WDAa1/usDhetWAMhgzF/Cnf5ek0nK00m0YZGyc4LzgD0CROMASTWNg=='; + // Expected thumbprint for this cert + const expected = 'e606eced42fa3abd0c5693456384f5931b174707'; + assert.strictEqual(thumbprint(cert), expected); + }); + }); + + describe('getAttribute', function () { + const obj = { + a: { + b: { + c: 'value' + } + } + }; + + it('should retrieve nested attributes', function () { + assert.strictEqual(getAttribute(obj, 'a.b.c'), 'value'); + }); + + it('should return default value if attribute missing', function () { + assert.strictEqual(getAttribute(obj, 'a.b.d', 'default'), 'default'); + }); + + it('should return default value if path is invalid', function () { + assert.strictEqual(getAttribute(obj, 'x.y.z', 'default'), 'default'); + }); + }); + + describe('isMultiRootedXMLError', function () { + it('should return true for multi-rooted error message', function () { + const err = { message: 'Only one element can be added and only after doctype' }; + assert.strictEqual(isMultiRootedXMLError(err), true); + }); + + it('should return false for other errors', function () { + const err = { message: 'Other error' }; + assert.strictEqual(isMultiRootedXMLError(err), false); + }); + }); +}); From 01b995f727530cca1709eae34d4a81d3926f5b95 Mon Sep 17 00:00:00 2001 From: Deepak Prabhakara Date: Wed, 19 Nov 2025 10:20:32 +0000 Subject: [PATCH 2/3] more tests --- test/lib/decrypt.spec.ts | 14 +++++++++ test/lib/getVersion.spec.ts | 4 +++ test/lib/metadata.spec.ts | 63 +++++++++++++++++++++++++++++++++++++ test/lib/response.spec.ts | 30 ++++++++++++++++++ test/lib/saml20.spec.ts | 21 +++++++------ test/lib/sign.spec.ts | 3 +- test/lib/utils.spec.ts | 25 +++++++++------ 7 files changed, 141 insertions(+), 19 deletions(-) diff --git a/test/lib/decrypt.spec.ts b/test/lib/decrypt.spec.ts index 9e36c047..122ea7b3 100644 --- a/test/lib/decrypt.spec.ts +++ b/test/lib/decrypt.spec.ts @@ -46,4 +46,18 @@ describe('decrypt.ts', function () { assert.strictEqual((error as Error).message, 'Exception of Assertion Decryption.'); } }); + + it('multiple assertions', function () { + const multipleAssertionsXml = ` + + Assertion1 + Assertion2 + + `; + try { + decryptXml(multipleAssertionsXml, options); + } catch (error) { + assert.strictEqual((error as Error).message, 'Multiple Assertion.'); + } + }); }); diff --git a/test/lib/getVersion.spec.ts b/test/lib/getVersion.spec.ts index 90c115a5..712ea860 100644 --- a/test/lib/getVersion.spec.ts +++ b/test/lib/getVersion.spec.ts @@ -5,4 +5,8 @@ describe('getVersion.ts', function () { it('getVersion not ok', function () { assert.strictEqual(version.getVersion(undefined), null); }); + + it('getVersion not 2.0', function () { + assert.strictEqual(version.getVersion({ '@': { Version: '1.1' } }), null); + }); }); diff --git a/test/lib/metadata.spec.ts b/test/lib/metadata.spec.ts index 9f67e61a..27339cb4 100644 --- a/test/lib/metadata.spec.ts +++ b/test/lib/metadata.spec.ts @@ -209,4 +209,67 @@ describe('metadata.ts', function () { assert.strictEqual(!!res, true); }); + it(`createSPMetadataXML ok with encryption`, async () => { + const res = createSPMetadataXML({ + acsUrl: 'http://localhost:4000/api/saml/sso', + entityId: 'https://saml.example.com/entityid', + publicKeyString: 'x509cert', + encryption: true, + }); + + assert.strictEqual(!!res, true); + assert(res.includes('md:EncryptionMethod')); + }); + + it('saml MetaData validateNameIDFormat not ok', async function () { + try { + await parseMetadata(samlMetadata, { + validateNameIDFormat: 'invalid:format', + }); + } catch (error) { + assert.strictEqual( + (error as Error).message, + "Invalid nameIDFormat. Please set 'Name ID Format' to undefined" + ); + } + }); + + it('saml MetaData with SLO services', async function () { + const sloMetadata = ` + + + + + + MIICajCCAdOgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBSMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRcwFQYDVQQDDA5zcC5leGFtcGxlLmNvbTAeFw0xNDA3MTcxNDEyNTZaFw0xNTA3MTcxNDEyNTZaMFIxCzAJBgNVBAYTAnVzMRMwEQYDVQQIDApDYWxpZm9ybmlhMRUwEwYDVQQKDAxPbmVsb2dpbiBJbmMxFzAVBgNVBAMMDnNwLmV4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDZx+ON4IUoIWxgukTb1tOiX3bMYzYQiwWPUNMp+Fq82xoNogso2bykZG0yiJm5o8zv/sd6pGouayMgkx/2FSOdc36T0jGbCHuRSbtia0PEzNIRtmViMrt3AeoWBidRXmZsxCNLwgIV6dn2WpuE5Az0bHgpZnQxTKFek0BMKU/d8wIDAQABo1AwTjAdBgNVHQ4EFgQUGHxYqZYyX7cTxKVODVgZwSTdCnwwHwYDVR0jBBgwFoAUGHxYqZYyX7cTxKVODVgZwSTdCnwwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOBgQByFOl+hMFICbd3DJfnp2Rgd/dqttsZG/tyhILWvErbio/DEe98mXpowhTkC04ENprOyXi7ZbUqiicF89uAGyt1oqgTUCD1VsLahqIcmrzgumNyTwLGWo17WDAa1/usDhetWAMhgzF/Cnf5ek0nK00m0YZGyc4LzgD0CROMASTWNg== + + + + + + + + `; + const value = await parseMetadata(sloMetadata, {}); + assert.strictEqual(value.slo.redirectUrl, 'http://localhost:4000/api/saml/slo/redirect'); + assert.strictEqual(value.slo.postUrl, 'http://localhost:4000/api/saml/slo/post'); + }); + + it('saml MetaData with SP descriptor', async function () { + const spMetadata = ` + + + + + + MIICajCCAdOgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBSMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRcwFQYDVQQDDA5zcC5leGFtcGxlLmNvbTAeFw0xNDA3MTcxNDEyNTZaFw0xNTA3MTcxNDEyNTZaMFIxCzAJBgNVBAYTAnVzMRMwEQYDVQQIDApDYWxpZm9ybmlhMRUwEwYDVQQKDAxPbmVsb2dpbiBJbmMxFzAVBgNVBAMMDnNwLmV4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDZx+ON4IUoIWxgukTb1tOiX3bMYzYQiwWPUNMp+Fq82xoNogso2bykZG0yiJm5o8zv/sd6pGouayMgkx/2FSOdc36T0jGbCHuRSbtia0PEzNIRtmViMrt3AeoWBidRXmZsxCNLwgIV6dn2WpuE5Az0bHgpZnQxTKFek0BMKU/d8wIDAQABo1AwTjAdBgNVHQ4EFgQUGHxYqZYyX7cTxKVODVgZwSTdCnwwHwYDVR0jBBgwFoAUGHxYqZYyX7cTxKVODVgZwSTdCnwwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOBgQByFOl+hMFICbd3DJfnp2Rgd/dqttsZG/tyhILWvErbio/DEe98mXpowhTkC04ENprOyXi7ZbUqiicF89uAGyt1oqgTUCD1VsLahqIcmrzgumNyTwLGWo17WDAa1/usDhetWAMhgzF/Cnf5ek0nK00m0YZGyc4LzgD0CROMASTWNg== + + + + + + `; + const value = await parseMetadata(spMetadata, {}); + assert.strictEqual(value.loginType, 'sp'); + }); }); diff --git a/test/lib/response.spec.ts b/test/lib/response.spec.ts index 20027d67..c04a406a 100644 --- a/test/lib/response.spec.ts +++ b/test/lib/response.spec.ts @@ -362,3 +362,33 @@ it('Should create a SAML response with nameFormat basic', async function () { true ); }); + +it('validate should throw error if both publicKey and thumbprint are provided', async function () { + try { + await validate(validResponse, { + publicKey: certificate, + thumbprint: 'thumbprint', + }); + } catch (error) { + assert.strictEqual( + (error as Error).message, + 'You should provide either cert or certThumbprint, not both' + ); + } +}); + +it('validate should throw error if rawAssertion is empty', async function () { + try { + await validate('', { publicKey: certificate }); + } catch (error) { + assert.strictEqual((error as Error).message, 'rawAssertion is required.'); + } +}); + +it('parse should throw error if rawAssertion is empty', async function () { + try { + await parse(''); + } catch (error) { + assert.strictEqual((error as Error).message, 'rawAssertion is required.'); + } +}); diff --git a/test/lib/saml20.spec.ts b/test/lib/saml20.spec.ts index da581193..599099e6 100644 --- a/test/lib/saml20.spec.ts +++ b/test/lib/saml20.spec.ts @@ -133,27 +133,30 @@ describe('saml20.ts', function () { const attributes = [ { '@': { Name: 'name', FriendlyName: 'email' }, - 'AttributeValue': { _: 'test@example.com' } + AttributeValue: { _: 'test@example.com' }, }, { '@': { Name: 'givenName', FriendlyName: 'givenName' }, - 'AttributeValue': { _: 'John' } + AttributeValue: { _: 'John' }, }, { '@': { Name: 'surname', FriendlyName: 'sn' }, - 'AttributeValue': { _: 'Doe' } - } + AttributeValue: { _: 'Doe' }, + }, ]; - + const assertionWithFriendlyNames = { AttributeStatement: { - Attribute: attributes - } + Attribute: attributes, + }, }; - + const result = saml20.parse(assertionWithFriendlyNames); const claims = result.claims as any; - assert.strictEqual(claims['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'], 'test@example.com'); + assert.strictEqual( + claims['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'], + 'test@example.com' + ); assert.strictEqual(claims['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname'], 'John'); assert.strictEqual(claims['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname'], 'Doe'); }); diff --git a/test/lib/sign.spec.ts b/test/lib/sign.spec.ts index 0fc74adc..4a535c52 100644 --- a/test/lib/sign.spec.ts +++ b/test/lib/sign.spec.ts @@ -2,7 +2,8 @@ import assert from 'assert'; import { sign } from '../../lib/sign'; import fs from 'fs'; -const validXml = 'me'; +const validXml = + 'me'; const signingKey = fs.readFileSync('./test/assets/certificates/oktaPrivateKey.pem').toString(); const publicKey = fs.readFileSync('./test/assets/certificates/oktaPublicKey.crt').toString(); diff --git a/test/lib/utils.spec.ts b/test/lib/utils.spec.ts index 044ddb23..e70cdfb8 100644 --- a/test/lib/utils.spec.ts +++ b/test/lib/utils.spec.ts @@ -1,5 +1,11 @@ import assert from 'assert'; -import { parseFromString, thumbprint, getAttribute, isMultiRootedXMLError, multiRootedXMLError } from '../../lib/utils'; +import { + parseFromString, + thumbprint, + getAttribute, + isMultiRootedXMLError, + multiRootedXMLError, +} from '../../lib/utils'; describe('utils.ts', function () { describe('parseFromString', function () { @@ -18,15 +24,16 @@ describe('utils.ts', function () { it('should throw error for multi-rooted XML', function () { assert.throws(() => parseFromString(''), /multirooted xml not allowed/); }); - + it('should throw error for empty XML', function () { - assert.throws(() => parseFromString(''), /missing root element/); + assert.throws(() => parseFromString(''), /missing root element/); }); }); describe('thumbprint', function () { it('should calculate correct thumbprint', function () { - const cert = 'MIICajCCAdOgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBSMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRcwFQYDVQQDDA5zcC5leGFtcGxlLmNvbTAeFw0xNDA3MTcxNDEyNTZaFw0xNTA3MTcxNDEyNTZaMFIxCzAJBgNVBAYTAnVzMRMwEQYDVQQIDApDYWxpZm9ybmlhMRUwEwYDVQQKDAxPbmVsb2dpbiBJbmMxFzAVBgNVBAMMDnNwLmV4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDZx+ON4IUoIWxgukTb1tOiX3bMYzYQiwWPUNMp+Fq82xoNogso2bykZG0yiJm5o8zv/sd6pGouayMgkx/2FSOdc36T0jGbCHuRSbtia0PEzNIRtmViMrt3AeoWBidRXmZsxCNLwgIV6dn2WpuE5Az0bHgpZnQxTKFek0BMKU/d8wIDAQABo1AwTjAdBgNVHQ4EFgQUGHxYqZYyX7cTxKVODVgZwSTdCnwwHwYDVR0jBBgwFoAUGHxYqZYyX7cTxKVODVgZwSTdCnwwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOBgQByFOl+hMFICbd3DJfnp2Rgd/dqttsZG/tyhILWvErbio/DEe98mXpowhTkC04ENprOyXi7ZbUqiicF89uAGyt1oqgTUCD1VsLahqIcmrzgumNyTwLGWo17WDAa1/usDhetWAMhgzF/Cnf5ek0nK00m0YZGyc4LzgD0CROMASTWNg=='; + const cert = + 'MIICajCCAdOgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBSMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRcwFQYDVQQDDA5zcC5leGFtcGxlLmNvbTAeFw0xNDA3MTcxNDEyNTZaFw0xNTA3MTcxNDEyNTZaMFIxCzAJBgNVBAYTAnVzMRMwEQYDVQQIDApDYWxpZm9ybmlhMRUwEwYDVQQKDAxPbmVsb2dpbiBJbmMxFzAVBgNVBAMMDnNwLmV4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDZx+ON4IUoIWxgukTb1tOiX3bMYzYQiwWPUNMp+Fq82xoNogso2bykZG0yiJm5o8zv/sd6pGouayMgkx/2FSOdc36T0jGbCHuRSbtia0PEzNIRtmViMrt3AeoWBidRXmZsxCNLwgIV6dn2WpuE5Az0bHgpZnQxTKFek0BMKU/d8wIDAQABo1AwTjAdBgNVHQ4EFgQUGHxYqZYyX7cTxKVODVgZwSTdCnwwHwYDVR0jBBgwFoAUGHxYqZYyX7cTxKVODVgZwSTdCnwwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOBgQByFOl+hMFICbd3DJfnp2Rgd/dqttsZG/tyhILWvErbio/DEe98mXpowhTkC04ENprOyXi7ZbUqiicF89uAGyt1oqgTUCD1VsLahqIcmrzgumNyTwLGWo17WDAa1/usDhetWAMhgzF/Cnf5ek0nK00m0YZGyc4LzgD0CROMASTWNg=='; // Expected thumbprint for this cert const expected = 'e606eced42fa3abd0c5693456384f5931b174707'; assert.strictEqual(thumbprint(cert), expected); @@ -37,9 +44,9 @@ describe('utils.ts', function () { const obj = { a: { b: { - c: 'value' - } - } + c: 'value', + }, + }, }; it('should retrieve nested attributes', function () { @@ -49,9 +56,9 @@ describe('utils.ts', function () { it('should return default value if attribute missing', function () { assert.strictEqual(getAttribute(obj, 'a.b.d', 'default'), 'default'); }); - + it('should return default value if path is invalid', function () { - assert.strictEqual(getAttribute(obj, 'x.y.z', 'default'), 'default'); + assert.strictEqual(getAttribute(obj, 'x.y.z', 'default'), 'default'); }); }); From 26c78a7d22384a2b3c568523c3ee628b2170bbaa Mon Sep 17 00:00:00 2001 From: Deepak Prabhakara Date: Wed, 19 Nov 2025 10:28:24 +0000 Subject: [PATCH 3/3] lint --- test/lib/utils.spec.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/test/lib/utils.spec.ts b/test/lib/utils.spec.ts index e70cdfb8..f4a067b1 100644 --- a/test/lib/utils.spec.ts +++ b/test/lib/utils.spec.ts @@ -1,11 +1,5 @@ import assert from 'assert'; -import { - parseFromString, - thumbprint, - getAttribute, - isMultiRootedXMLError, - multiRootedXMLError, -} from '../../lib/utils'; +import { parseFromString, thumbprint, getAttribute, isMultiRootedXMLError } from '../../lib/utils'; describe('utils.ts', function () { describe('parseFromString', function () {