diff --git a/Readme.md b/Readme.md
index c559c179d..56e7b6a02 100644
--- a/Readme.md
+++ b/Readme.md
@@ -50,6 +50,8 @@ This module lets you connect to web services using SOAP. It also provides a ser
- [ClientSSLSecurityPFX](#clientsslsecuritypfx)
- [WSSecurity](#wssecurity)
- [WSSecurityCert](#wssecuritycert)
+ - [WSSecurityPlusCert](#wssecuritypluscert)
+ - [WSSecurityCertWithToken](#wssecuritycertwithtoken)
- [NTLMSecurity](#ntlmsecurity)
- [Handling XML Attributes, Value and XML (wsdlOptions).](#handling-xml-attributes-value-and-xml-wsdloptions)
- [Overriding the `value` key](#overriding-the-value-key)
@@ -1081,6 +1083,16 @@ Use WSSecurity and WSSecurityCert together.
```
+### WSSecurityCertWithToken
+
+WS-Security X509 Certificate support. Just like WSSecurityCert, except that it accepts the input properties as a single object, with two properties added `username` and `password`. Which if added, will add a UsernameToken Element to the xml security element.
+
+``` xml
+
+ someusername
+ someusername's password
+
+```
### NTLMSecurity
diff --git a/src/security/WSSecurityCertWithToken.ts b/src/security/WSSecurityCertWithToken.ts
new file mode 100644
index 000000000..1eac9a4df
--- /dev/null
+++ b/src/security/WSSecurityCertWithToken.ts
@@ -0,0 +1,167 @@
+import { v4 as uuidv4 } from 'uuid';
+import { SignedXml } from 'xml-crypto';
+import { ISecurity } from '../types';
+import { IWSSecurityCertOptions, IXmlSignerOptions } from './WSSecurityCert';
+
+function addMinutes(date: Date, minutes: number) {
+ return new Date(date.getTime() + minutes * 60000);
+}
+
+function dateStringForSOAP(date: Date): string {
+ return date.getUTCFullYear() + '-' + ('0' + (date.getUTCMonth() + 1)).slice(-2) + '-' +
+ ('0' + date.getUTCDate()).slice(-2) + 'T' + ('0' + date.getUTCHours()).slice(-2) + ':' +
+ ('0' + date.getUTCMinutes()).slice(-2) + ':' + ('0' + date.getUTCSeconds()).slice(-2) + 'Z';
+}
+
+function generateCreated(): string {
+ return dateStringForSOAP(new Date());
+}
+
+function generateExpires(): string {
+ return dateStringForSOAP(addMinutes(new Date(), 10));
+}
+
+function insertStr(src: string, dst: string, pos: number): string {
+ return [dst.slice(0, pos), src, dst.slice(pos)].join('');
+}
+
+function generateId(): string {
+ return uuidv4().replace(/-/gm, '');
+}
+
+function resolvePlaceholderInReferences(references: any[], bodyXpath: string) {
+ for (const ref of references) {
+ if (ref.xpath === bodyXpathPlaceholder) {
+ ref.xpath = bodyXpath;
+ }
+ }
+}
+
+const oasisBaseUri = 'http://docs.oasis-open.org/wss/2004/01';
+const bodyXpathPlaceholder = '[[bodyXpath]]';
+
+export class WSSecurityCertWithToken implements ISecurity {
+ private publicP12PEM: string;
+ private signer: any;
+ private signerOptions: IXmlSignerOptions = {};
+ private x509Id: string;
+ private hasTimeStamp: boolean;
+ private signatureTransformations: string[];
+ private created: string;
+ private expires: string;
+ private additionalReferences: string[] = [];
+ private username: string;
+ private password: string;
+
+ constructor(props: { privateKey: Buffer, publicKey: string, keyPassword?: string, username: string, password: string, options?: IWSSecurityCertOptions }) {
+ this.publicP12PEM = props.publicKey.toString()
+ .replace('-----BEGIN CERTIFICATE-----', '')
+ .replace('-----END CERTIFICATE-----', '')
+ .replace(/(\r\n|\n|\r)/gm, '');
+ this.username = props.username;
+ this.password = props.password;
+
+ this.signer = new SignedXml();
+ const opts = props.options || {};
+ if (opts.signatureAlgorithm === 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256') {
+ this.signer.signatureAlgorithm = opts.signatureAlgorithm;
+ this.signer.addReference(
+ bodyXpathPlaceholder,
+ ['http://www.w3.org/2001/10/xml-exc-c14n#'],
+ 'http://www.w3.org/2001/04/xmlenc#sha256',
+ );
+ }
+
+ if (opts.additionalReferences && opts.additionalReferences.length > 0) {
+ this.additionalReferences = opts.additionalReferences;
+ }
+
+ if (opts.signerOptions) {
+ const { signerOptions } = props.options;
+ this.signerOptions = signerOptions;
+ if (!this.signerOptions.existingPrefixes) {
+ this.signerOptions.existingPrefixes = {};
+ }
+ if (this.signerOptions.existingPrefixes && !this.signerOptions.existingPrefixes.wsse) {
+ this.signerOptions.existingPrefixes.wsse = `${oasisBaseUri}/oasis-200401-wss-wssecurity-secext-1.0.xsd`;
+ }
+ } else {
+ this.signerOptions = { existingPrefixes: { wsse: `${oasisBaseUri}/oasis-200401-wss-wssecurity-secext-1.0.xsd` } };
+ }
+
+ this.signer.signingKey = {
+ key: props.privateKey,
+ passphrase: props.keyPassword,
+ };
+ this.x509Id = `x509-${generateId()}`;
+ this.hasTimeStamp = typeof opts.hasTimeStamp === 'undefined' ? true : !!opts.hasTimeStamp;
+ this.signatureTransformations = Array.isArray(opts.signatureTransformations) ? opts.signatureTransformations
+ : ['http://www.w3.org/2000/09/xmldsig#enveloped-signature', 'http://www.w3.org/2001/10/xml-exc-c14n#'];
+
+ this.signer.keyInfoProvider = {};
+ this.signer.keyInfoProvider.getKeyInfo = (key) => {
+ return `` +
+ `` +
+ ``;
+ };
+ }
+
+ public postProcess(xml, envelopeKey) {
+ this.created = generateCreated();
+ this.expires = generateExpires();
+
+ let timestampStr = '';
+ if (this.hasTimeStamp) {
+ timestampStr =
+ `` +
+ `${this.created}` +
+ `${this.expires}` +
+ ``;
+ }
+ let usernameToken = '';
+ if (this.username) {
+ usernameToken = `` +
+ `${this.username} ` +
+ `${this.password} ` +
+ ``;
+ }
+ const secHeader =
+ `` +
+ `${this.publicP12PEM}` +
+ usernameToken +
+ timestampStr +
+ ``;
+
+ const xmlWithSec = insertStr(secHeader, xml, xml.indexOf(`${envelopeKey}:Header>`));
+
+ const references = this.signatureTransformations;
+
+ const bodyXpath = `//*[name(.)='${envelopeKey}:Body']`;
+ resolvePlaceholderInReferences(this.signer.references, bodyXpath);
+
+ if (!(this.signer.references.filter((ref) => (ref.xpath === bodyXpath)).length > 0)) {
+ this.signer.addReference(bodyXpath, references);
+ }
+
+ for (const name of this.additionalReferences) {
+ const xpath = `//*[name(.)='${name}']`;
+ if (!(this.signer.references.filter((ref) => (ref.xpath === xpath)).length > 0)) {
+ this.signer.addReference(xpath, references);
+ }
+ }
+
+ const timestampXpath = `//*[name(.)='wsse:Security']/*[local-name(.)='Timestamp']`;
+ if (this.hasTimeStamp && !(this.signer.references.filter((ref) => (ref.xpath === timestampXpath)).length > 0)) {
+ this.signer.addReference(timestampXpath, references);
+ }
+
+ this.signer.computeSignature(xmlWithSec, this.signerOptions);
+
+ return insertStr(this.signer.getSignatureXml(), xmlWithSec, xmlWithSec.indexOf(''));
+ }
+}
diff --git a/src/security/index.ts b/src/security/index.ts
index bb1cc2510..7cfc42126 100644
--- a/src/security/index.ts
+++ b/src/security/index.ts
@@ -6,4 +6,5 @@ export * from './ClientSSLSecurityPFX';
export * from './NTLMSecurity';
export * from './WSSecurity';
export * from './WSSecurityCert';
+export * from './WSSecurityCertWithToken';
export * from './WSSecurityPlusCert';
diff --git a/src/soap.ts b/src/soap.ts
index c86f71bc9..e2df58abf 100644
--- a/src/soap.ts
+++ b/src/soap.ts
@@ -15,7 +15,7 @@ const debug = debugBuilder('node-soap:soap');
export const security = _security;
export { Client } from './client';
export { HttpClient } from './http';
-export { BasicAuthSecurity, BearerSecurity, ClientSSLSecurity, ClientSSLSecurityPFX, NTLMSecurity, WSSecurity, WSSecurityCert, WSSecurityPlusCert } from './security';
+export { BasicAuthSecurity, BearerSecurity, ClientSSLSecurity, ClientSSLSecurityPFX, NTLMSecurity, WSSecurity, WSSecurityCert, WSSecurityPlusCert, WSSecurityCertWithToken } from './security';
export { Server } from './server';
export { passwordDigest } from './utils';
export * from './types';
diff --git a/src/wsdl/index.ts b/src/wsdl/index.ts
index 7a258c5e0..3438c8b1b 100644
--- a/src/wsdl/index.ts
+++ b/src/wsdl/index.ts
@@ -1274,6 +1274,8 @@ export class WSDL {
types.addChild(schema);
root.addChild(types);
stack.push(schema);
+ } else if (name === 'html') {
+ throw new Error(`Root element of WSDL was . This is likely an authentication issue.`);
} else {
throw new Error('Unexpected root element of WSDL or include');
}
diff --git a/test/client-test.js b/test/client-test.js
index 74ba50269..e93733588 100644
--- a/test/client-test.js
+++ b/test/client-test.js
@@ -1671,6 +1671,10 @@ xit('should add namespace to array of objects', function (done) {
}
done();
});
+ })
+ .catch(function (err) {
+ assert.equal(err.message, 'Root element of WSDL was . This is likely an authentication issue.');
+ done();
});
});
diff --git a/test/security/WSSecurityCertWithToken.js b/test/security/WSSecurityCertWithToken.js
new file mode 100644
index 000000000..652f7c864
--- /dev/null
+++ b/test/security/WSSecurityCertWithToken.js
@@ -0,0 +1,245 @@
+"use strict";
+
+var fs = require("fs"),
+ join = require("path").join;
+
+describe("WSSecurityCertWithToken", function () {
+ var WSSecurityCertWithToken = require("../../lib/soap").WSSecurityCertWithToken;
+ var cert = fs.readFileSync(join(__dirname, "..", "certs", "agent2-cert.pem"));
+ var key = fs.readFileSync(join(__dirname, "..", "certs", "agent2-key.pem"));
+
+
+ it("should take parameters as options object", function () {
+ var instance = new WSSecurityCertWithToken({
+ privateKey: key,
+ publicKey: cert,
+ keyPassword: "",
+ options: {}
+ });
+ var xml = instance.postProcess(
+ "",
+ "soap"
+ );
+ xml.should.containEql("",
+ "soap"
+ );
+ xml.should.containEql("testuser");
+ xml.should.containEql("testpw");
+ });
+ describe("should pass all the WSSecurityCert tests too", ()=>{
+ var keyWithPassword = fs.readFileSync(
+ join(__dirname, "..", "certs", "agent2-key-with-password.pem")
+ ); // The passphrase protecting the private key is "soap"
+ it('is a function', function () {
+ WSSecurityCertWithToken.should.be.type('function');
+ });
+
+ it('should accept valid constructor variables', function () {
+ var instance = new WSSecurityCertWithToken({privateKey: key, publicKey: cert, keyPassword: ''});
+ instance.should.have.property('publicP12PEM');
+ instance.should.have.property('signer');
+ instance.should.have.property('x509Id');
+ });
+
+ it('should fail at computing signature when the private key is invalid', function () {
+ var passed = true;
+
+ try {
+ var instance = new WSSecurityCertWithToken({privateKey: '*****', publicKey: cert, keyPassword: ''});
+ instance.postProcess('', 'soap');
+ } catch (e) {
+ passed = false;
+ }
+
+ if (passed) {
+ throw new Error('bad private key');
+ }
+ });
+
+ it('should insert a WSSecurity signing block when postProcess is called (private key is raw)', function () {
+ var instance = new WSSecurityCertWithToken({privateKey: key, publicKey: cert, keyPassword: ''});
+ var xml = instance.postProcess('', 'soap');
+
+ xml.should.containEql('');
+ xml.should.containEql('');
+ xml.should.containEql('' + instance.created);
+ xml.should.containEql('' + instance.expires);
+ xml.should.containEql('');
+ xml.should.containEql('');
+ xml.should.containEql(instance.publicP12PEM);
+ xml.should.containEql(instance.signer.getSignatureXml());
+ });
+
+ it('should insert a WSSecurity signing block when postProcess is called (private key is protected by a passphrase)', function () {
+ var instance = new WSSecurityCertWithToken({privateKey: keyWithPassword, publicKey: cert, keyPassword: 'soap'});
+ var xml = instance.postProcess('', 'soap');
+
+ xml.should.containEql('');
+ xml.should.containEql('');
+ xml.should.containEql('' + instance.created);
+ xml.should.containEql('' + instance.expires);
+ xml.should.containEql('');
+ xml.should.containEql('');
+ xml.should.containEql(instance.publicP12PEM);
+ xml.should.containEql(instance.signer.getSignatureXml());
+ });
+
+ it('should only add two Reference elements, for Soap Body and Timestamp inside wsse:Security element', function () {
+ var instance = new WSSecurityCertWithToken({privateKey: key, publicKey: cert, keyPassword: ''});
+ var xml = instance.postProcess('', 'soap');
+ xml.match(/', 'soap');
+ xml.match(/', 'soap');
+ xml.should.not.containEql('');
+ xml.should.not.containEql('' + instance.created);
+ xml.should.not.containEql('' + instance.expires);
+ });
+
+ it('should use rsa-sha256 signature method when the signatureAlgorithm option is set to WSSecurityCert', function () {
+ var instance = new WSSecurityCertWithToken({privateKey: key, publicKey: cert, keyPassword: '', options: {
+ hasTimeStamp: false,
+ signatureAlgorithm: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'
+ }});
+ var xml = instance.postProcess('', 'soap');
+ xml.should.containEql('SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"');
+ });
+
+ it('should use default xmlns:wsse if no signerOptions.existingPrefixes is provided', function () {
+ var instance = new WSSecurityCertWithToken({privateKey: key, publicKey: cert, keyPassword: ''});
+ var xml = instance.postProcess('', 'soap')
+ xml.should.containEql('xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"');
+ });
+ it('should still add wsse if another signerOption attribute is passed through ', function(){
+ var instance = new WSSecurityCertWithToken({privateKey: key, publicKey: cert, keyPassword: '', options: { signerOptions: { prefix: 'ds'} }});
+ var xml = instance.postProcess('', 'soap')
+ xml.should.containEql('xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"');
+ xml.should.containEql('');
+ });
+ it('should contain a provided prefix when signerOptions.existingPrefixes is provided', function () {
+ var instance = new WSSecurityCertWithToken({privateKey: key, publicKey: cert, keyPassword: '', options: {
+ signerOptions: {
+ location: { action: 'after' },
+ existingPrefixes: { wsse: 'https://localhost/node-soap.xsd' }
+ }
+ }});
+ var xml = instance.postProcess('', 'soap')
+ xml.should.containEql('');
+ });
+ it('should contain the prefix to the generated Signature tags', function () {
+ var instance = new WSSecurityCertWithToken({privateKey: key, publicKey: cert, keyPassword: '', options: {
+ signerOptions: {
+ prefix: 'ds',
+ }
+ }});
+ var xml = instance.postProcess('', 'soap');
+ xml.should.containEql('');
+ xml.should.containEql('');
+ xml.should.containEql('');
+ xml.should.containEql('');
+ xml.should.containEql('');
+ xml.should.containEql('');
+ });
+ it('should add attributes to the security tag', function () {
+ var instance = new WSSecurityCertWithToken({privateKey: key, publicKey: cert, keyPassword: '', options: {
+ signerOptions: {
+ attrs: { Id: 'security_123' },
+ }
+ }});
+ var xml = instance.postProcess('', 'soap');
+ xml.should.containEql('');
+ });
+ it('should sign additional headers that are added via additionalReferences', function () {
+ var instance = new WSSecurityCertWithToken({privateKey: key, publicKey: cert, keyPassword: '', options: {
+ additionalReferences: [
+ 'To',
+ 'Action'
+ ],
+ }});
+ var xml = instance.postProcess('localhost.comtesting', 'soap');
+ xml.should.containEql('');
+ xml.should.containEql('');
+ });
+ it('should add a WSSecurity signing block when valid envelopeKey is passed', function () {
+ var instance = new WSSecurityCertWithToken({privateKey: key, publicKey: cert, keyPassword: ''});
+ var xml = instance.postProcess('', 'soapenv');
+ xml.should.containEql('', 'soapenv');
+ xml.should.containEql('soapenv:mustUnderstand="1"');
+ });
+ it('should not accept envelopeKey not set in envelope', function () {
+ var xml;
+ try {
+ var instance = new WSSecurityCertWithToken({privateKey: key, publicKey: cert, keyPassword: ''});
+ xml = instance.postProcess('', 'soap');
+ } catch (e) {
+ // do nothing
+ }
+ should(xml).not.be.ok();
+ });
+ })
+});
diff --git a/test/wsdl-test.js b/test/wsdl-test.js
index 0c3cf4c8c..49fee457c 100644
--- a/test/wsdl-test.js
+++ b/test/wsdl-test.js
@@ -205,9 +205,13 @@ describe('WSDL Parser (non-strict)', () => {
if (!/.wsdl$/.exec(file)) return;
it('should parse and describe '+file, (done) => {
soap.createClient(__dirname+'/wsdl/'+file, function(err, client) {
- assert.ifError(err);
- client.describe();
- done();
+ if (err && err.message === 'Root element of WSDL was . This is likely an authentication issue.') {
+ done();
+ } else {
+ assert.ifError(err);
+ client.describe();
+ done();
+ }
});
});
});
diff --git a/tsconfig.json b/tsconfig.json
index 0f984c65e..e5d30d5d3 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -15,4 +15,4 @@
"exclude": [
"node_modules"
]
-}
+}
\ No newline at end of file